Add OAuthenticator
This commit is contained in:
@@ -465,6 +465,7 @@ set(SOURCES
|
|||||||
src/core/standardpaths.cpp
|
src/core/standardpaths.cpp
|
||||||
src/core/httpbaserequest.cpp
|
src/core/httpbaserequest.cpp
|
||||||
src/core/jsonbaserequest.cpp
|
src/core/jsonbaserequest.cpp
|
||||||
|
src/core/oauthenticator.cpp
|
||||||
|
|
||||||
src/utilities/strutils.cpp
|
src/utilities/strutils.cpp
|
||||||
src/utilities/envutils.cpp
|
src/utilities/envutils.cpp
|
||||||
@@ -862,6 +863,7 @@ set(HEADERS
|
|||||||
src/core/songmimedata.h
|
src/core/songmimedata.h
|
||||||
src/core/httpbaserequest.h
|
src/core/httpbaserequest.h
|
||||||
src/core/jsonbaserequest.h
|
src/core/jsonbaserequest.h
|
||||||
|
src/core/oauthenticator.h
|
||||||
|
|
||||||
src/tagreader/tagreaderclient.h
|
src/tagreader/tagreaderclient.h
|
||||||
src/tagreader/tagreaderreply.h
|
src/tagreader/tagreaderreply.h
|
||||||
|
|||||||
609
src/core/oauthenticator.cpp
Normal file
609
src/core/oauthenticator.cpp
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2022-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonParseError>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
|
#include "constants/timeconstants.h"
|
||||||
|
#include "utilities/randutils.h"
|
||||||
|
#include "logging.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "networkaccessmanager.h"
|
||||||
|
#include "localredirectserver.h"
|
||||||
|
#include "oauthenticator.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
using std::make_shared;
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr char kTokenType[] = "token_type";
|
||||||
|
constexpr char kAccessToken[] = "access_token";
|
||||||
|
constexpr char kRefreshToken[] = "refresh_token";
|
||||||
|
constexpr char kExpiresIn[] = "expires_in";
|
||||||
|
constexpr char kLoginTime[] = "login_time";
|
||||||
|
constexpr char kUserId[] = "user_id";
|
||||||
|
constexpr char kCountryCode[] = "country_code";
|
||||||
|
constexpr int kMaxPortInc = 20;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
OAuthenticator::OAuthenticator(const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
network_(network),
|
||||||
|
timer_refresh_login_(new QTimer(this)),
|
||||||
|
type_(Type::Authorization_Code),
|
||||||
|
use_local_redirect_server_(true),
|
||||||
|
random_port_(true),
|
||||||
|
expires_in_(0LL),
|
||||||
|
login_time_(0LL),
|
||||||
|
user_id_(0) {
|
||||||
|
|
||||||
|
timer_refresh_login_->setSingleShot(true);
|
||||||
|
QObject::connect(timer_refresh_login_, &QTimer::timeout, this, &OAuthenticator::RenewAccessToken);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuthenticator::~OAuthenticator() {
|
||||||
|
|
||||||
|
while (!replies_.isEmpty()) {
|
||||||
|
QNetworkReply *reply = replies_.takeFirst();
|
||||||
|
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||||
|
reply->abort();
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_settings_group(const QString &settings_group) {
|
||||||
|
|
||||||
|
settings_group_ = settings_group;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_type(const Type type) {
|
||||||
|
|
||||||
|
type_ = type;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_authorize_url(const QUrl &authorize_url) {
|
||||||
|
|
||||||
|
authorize_url_ = authorize_url;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_redirect_url(const QUrl &redirect_url) {
|
||||||
|
|
||||||
|
redirect_url_ = redirect_url;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_access_token_url(const QUrl &access_token_url) {
|
||||||
|
|
||||||
|
access_token_url_ = access_token_url;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_client_id(const QString &client_id) {
|
||||||
|
|
||||||
|
client_id_ = client_id;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_client_secret(const QString &client_secret) {
|
||||||
|
|
||||||
|
client_secret_ = client_secret;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_scope(const QString &scope) {
|
||||||
|
|
||||||
|
scope_ = scope;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_use_local_redirect_server(const bool use_local_redirect_server) {
|
||||||
|
|
||||||
|
use_local_redirect_server_ = use_local_redirect_server;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::set_random_port(const bool random_port) {
|
||||||
|
|
||||||
|
random_port_ = random_port;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray OAuthenticator::authorization_header() const {
|
||||||
|
|
||||||
|
if (token_type_.isEmpty() || access_token_.isEmpty()) {
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return token_type().toUtf8() + " " + access_token().toUtf8();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OAuthenticator::GrantType() const {
|
||||||
|
|
||||||
|
switch (type_) {
|
||||||
|
case Type::Authorization_Code:
|
||||||
|
return u"authorization_code"_s;
|
||||||
|
break;
|
||||||
|
case Type::Client_Credentials:
|
||||||
|
return u"client_credentials"_s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::LoadSession() {
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(settings_group_);
|
||||||
|
token_type_ = s.value(kTokenType).toString();
|
||||||
|
access_token_ = s.value(kAccessToken).toString();
|
||||||
|
refresh_token_ = s.value(kRefreshToken).toString();
|
||||||
|
expires_in_ = s.value(kExpiresIn, 0LL).toLongLong();
|
||||||
|
login_time_ = s.value(kLoginTime, 0LL).toLongLong();
|
||||||
|
country_code_ = s.value(kCountryCode).toString();
|
||||||
|
user_id_ = s.value(kUserId).toInt();
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
StartRefreshLoginTimer();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::ClearSession() {
|
||||||
|
|
||||||
|
token_type_.clear();
|
||||||
|
access_token_.clear();
|
||||||
|
refresh_token_.clear();
|
||||||
|
expires_in_ = 0;
|
||||||
|
login_time_ = 0;
|
||||||
|
country_code_.clear();
|
||||||
|
user_id_ = 0;
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(settings_group_);
|
||||||
|
s.remove(kTokenType);
|
||||||
|
s.remove(kAccessToken);
|
||||||
|
s.remove(kRefreshToken);
|
||||||
|
s.remove(kExpiresIn);
|
||||||
|
s.remove(kLoginTime);
|
||||||
|
s.remove(kCountryCode);
|
||||||
|
s.remove(kUserId);
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
if (timer_refresh_login_->isActive()) {
|
||||||
|
timer_refresh_login_->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::StartRefreshLoginTimer() {
|
||||||
|
|
||||||
|
if (login_time_ > 0 && !refresh_token_.isEmpty() && expires_in_ > 0) {
|
||||||
|
const qint64 time = std::max(1LL, expires_in_ - (QDateTime::currentSecsSinceEpoch() - login_time_));
|
||||||
|
qLog(Debug) << settings_group_ << "Refreshing login in" << time << "seconds";
|
||||||
|
timer_refresh_login_->setInterval(static_cast<int>(time * kMsecPerSec));
|
||||||
|
if (!timer_refresh_login_->isActive()) {
|
||||||
|
timer_refresh_login_->start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::Authenticate() {
|
||||||
|
|
||||||
|
if (type_ == Type::Client_Credentials) {
|
||||||
|
RequestAccessToken();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl redirect_url(redirect_url_);
|
||||||
|
|
||||||
|
if (use_local_redirect_server_) {
|
||||||
|
local_redirect_server_.reset(new LocalRedirectServer(this));
|
||||||
|
bool success = false;
|
||||||
|
if (random_port_) {
|
||||||
|
success = local_redirect_server_->Listen();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const int max_port = redirect_url.port() + kMaxPortInc;
|
||||||
|
for (int port = redirect_url.port(); port < max_port; ++port) {
|
||||||
|
local_redirect_server_->set_port(port);
|
||||||
|
if (local_redirect_server_->Listen()) {
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, local_redirect_server_->error());
|
||||||
|
local_redirect_server_.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QObject::connect(&*local_redirect_server_, &LocalRedirectServer::Finished, this, &OAuthenticator::RedirectArrived);
|
||||||
|
redirect_url.setPort(local_redirect_server_->port());
|
||||||
|
}
|
||||||
|
|
||||||
|
code_verifier_ = Utilities::CryptographicRandomString(44);
|
||||||
|
code_challenge_ = QString::fromLatin1(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding));
|
||||||
|
if (code_challenge_.lastIndexOf(u'=') == code_challenge_.length() - 1) {
|
||||||
|
code_challenge_.chop(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ParamList params = ParamList() << Param(u"response_type"_s, u"code"_s)
|
||||||
|
<< Param(u"redirect_uri"_s, redirect_url.toString())
|
||||||
|
<< Param(u"state"_s, code_challenge_)
|
||||||
|
<< Param(u"code_challenge_method"_s, u"S256"_s)
|
||||||
|
<< Param(u"code_challenge"_s, code_challenge_);
|
||||||
|
|
||||||
|
if (!client_id_.isEmpty()) {
|
||||||
|
params << Param(u"client_id"_s, client_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scope_.isEmpty()) {
|
||||||
|
params << Param(u"scope"_s, scope_);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(params.begin(), params.end());
|
||||||
|
|
||||||
|
QUrlQuery url_query;
|
||||||
|
for (const Param ¶m : std::as_const(params)) {
|
||||||
|
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first, ";")), QString::fromLatin1(QUrl::toPercentEncoding(param.second, ";")));
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl url(authorize_url_);
|
||||||
|
url.setQuery(url_query);
|
||||||
|
|
||||||
|
qLog(Debug) << "OAuthenticator" << url.toDisplayString();
|
||||||
|
|
||||||
|
const bool success = QDesktopServices::openUrl(url);
|
||||||
|
if (!success) {
|
||||||
|
QMessageBox messagebox(QMessageBox::Information, tr("Authentication"), tr("Please open this URL in your browser") + QStringLiteral(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
|
||||||
|
messagebox.setTextFormat(Qt::RichText);
|
||||||
|
messagebox.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *OAuthenticator::CreateRequest(const ParamList ¶ms) {
|
||||||
|
|
||||||
|
QUrlQuery url_query;
|
||||||
|
for (const Param ¶m : std::as_const(params)) {
|
||||||
|
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkRequest network_request(access_token_url_);
|
||||||
|
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
||||||
|
if (type_ == Type::Client_Credentials && !client_id_.isEmpty() && !client_secret_.isEmpty()) {
|
||||||
|
const QString authorization_header = client_id_ + u':' + client_secret_;
|
||||||
|
network_request.setRawHeader("Authorization", "Basic " + authorization_header.toUtf8().toBase64());
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *reply = network_->post(network_request, url_query.toString(QUrl::FullyEncoded).toUtf8());
|
||||||
|
replies_ << reply;
|
||||||
|
QObject::connect(reply, &QNetworkReply::sslErrors, this, &OAuthenticator::HandleLoginSSLErrors);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AccessTokenRequestFinished(reply); });
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::RedirectArrived() {
|
||||||
|
|
||||||
|
if (local_redirect_server_.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local_redirect_server_->success()) {
|
||||||
|
QUrl redirect_url(redirect_url_);
|
||||||
|
redirect_url.setPort(local_redirect_server_->port());
|
||||||
|
AuthorizationUrlReceived(local_redirect_server_->request_url(), redirect_url);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Q_EMIT AuthenticationFinished(false, local_redirect_server_->error());
|
||||||
|
}
|
||||||
|
|
||||||
|
local_redirect_server_.reset();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::ExternalAuthorizationUrlReceived(const QUrl &request_url) {
|
||||||
|
|
||||||
|
AuthorizationUrlReceived(request_url, redirect_url_);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::AuthorizationUrlReceived(const QUrl &request_url, const QUrl &redirect_url) {
|
||||||
|
|
||||||
|
if (!request_url.isValid()) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, tr("Received invalid reply from web browser."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request_url.hasQuery()) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, tr("Redirect URL is missing query."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qLog(Debug) << "Authorization URL Received" << request_url.toDisplayString();
|
||||||
|
|
||||||
|
QUrlQuery url_query(request_url);
|
||||||
|
|
||||||
|
if (url_query.hasQueryItem(u"error_description"_s)) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, url_query.queryItemValue(u"error_description"_s, QUrl::FullyDecoded));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url_query.hasQueryItem(u"error"_s)) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, url_query.queryItemValue(u"error"_s));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url_query.hasQueryItem(u"code"_s)) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, tr("Request URL is missing code!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url_query.hasQueryItem(u"state"_s)) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, tr("Request URL is missing state!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url_query.queryItemValue(u"state"_s) != code_challenge_) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, tr("Request URL has wrong state %1 != %2").arg(url_query.queryItemValue(u"state"_s), code_challenge_));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestAccessToken(url_query.queryItemValue(u"code"_s), redirect_url);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::RequestAccessToken(const QString &code, const QUrl &redirect_url) {
|
||||||
|
|
||||||
|
if (timer_refresh_login_->isActive()) {
|
||||||
|
timer_refresh_login_->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
ParamList params = ParamList() << Param(u"grant_type"_s, GrantType());
|
||||||
|
|
||||||
|
if (!code.isEmpty()) {
|
||||||
|
params << Param(u"code"_s, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!code_verifier_.isEmpty()) {
|
||||||
|
params << Param(u"code_verifier"_s, code_verifier_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!code.isEmpty()) {
|
||||||
|
params << Param(u"redirect_uri"_s, redirect_url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client_id_.isEmpty()) {
|
||||||
|
params << Param(u"client_id"_s, client_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client_secret_.isEmpty()) {
|
||||||
|
params << Param(u"client_secret"_s, client_secret_);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(params.begin(), params.end());
|
||||||
|
|
||||||
|
CreateRequest(params);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::RenewAccessToken() {
|
||||||
|
|
||||||
|
if (timer_refresh_login_->isActive()) {
|
||||||
|
timer_refresh_login_->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client_id_.isEmpty() || refresh_token_.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParamList params = ParamList() << Param(u"grant_type"_s, u"refresh_token"_s)
|
||||||
|
<< Param(u"client_id"_s, client_id_)
|
||||||
|
<< Param(u"refresh_token"_s, refresh_token_);
|
||||||
|
|
||||||
|
if (!client_secret_.isEmpty()) {
|
||||||
|
params << Param(u"client_secret"_s, client_secret_);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateRequest(params);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::HandleLoginSSLErrors(const QList<QSslError> &ssl_errors) {
|
||||||
|
|
||||||
|
for (const QSslError &ssl_error : ssl_errors) {
|
||||||
|
qLog(Debug) << ssl_error.errorString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuthenticator::AccessTokenRequestFinished(QNetworkReply *reply) {
|
||||||
|
|
||||||
|
if (!replies_.contains(reply)) return;
|
||||||
|
replies_.removeAll(reply);
|
||||||
|
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||||
|
const QString error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||||
|
Q_EMIT AuthenticationFinished(false, error_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||||
|
const QByteArray data = reply->readAll();
|
||||||
|
if (!data.isEmpty()) {
|
||||||
|
QJsonParseError json_error;
|
||||||
|
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
|
||||||
|
if (json_error.error == QJsonParseError::NoError && !json_document.isEmpty() && json_document.isObject()) {
|
||||||
|
const QJsonObject json_object = json_document.object();
|
||||||
|
if (json_object.contains("error"_L1) && json_object.contains("error_description"_L1)) {
|
||||||
|
const QString error = json_object["error"_L1].toString();
|
||||||
|
const QString error_description = json_object["error_description"_L1].toString();
|
||||||
|
Q_EMIT AuthenticationFinished(false, QStringLiteral("%1 (%2)").arg(error, error_description));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qLog(Debug) << "Unknown Json reply" << json_object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, QStringLiteral("Received HTTP status code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Q_EMIT AuthenticationFinished(false, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray data = reply->readAll();
|
||||||
|
|
||||||
|
QJsonParseError json_error;
|
||||||
|
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
|
||||||
|
if (json_error.error != QJsonParseError::NoError) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, QStringLiteral("Failed to parse Json data in authentication reply: %1").arg(json_error.errorString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json_document.isEmpty()) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server has empty Json document."_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json_document.isObject()) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server has Json document that is not an object."_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject json_object = json_document.object();
|
||||||
|
if (json_object.isEmpty()) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server has empty Json object."_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json_object.contains("token_type"_L1)) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server is missing token type."_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json_object.contains("access_token"_L1)) {
|
||||||
|
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server is missing access token."_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
token_type_ = json_object["token_type"_L1].toString();
|
||||||
|
access_token_ = json_object["access_token"_L1].toString();
|
||||||
|
|
||||||
|
if (json_object.contains("refresh_token"_L1)) {
|
||||||
|
refresh_token_ = json_object["refresh_token"_L1].toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
refresh_token_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json_object.contains("expires_in"_L1)) {
|
||||||
|
expires_in_ = json_object["expires_in"_L1].toInt();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expires_in_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
login_time_ = QDateTime::currentSecsSinceEpoch();
|
||||||
|
country_code_.clear();
|
||||||
|
user_id_ = 0;
|
||||||
|
|
||||||
|
if (json_object.contains("user"_L1) && json_object["user"_L1].isObject()) {
|
||||||
|
const QJsonObject object_user = json_object["user"_L1].toObject();
|
||||||
|
if (object_user.contains("countryCode"_L1) && object_user.contains("userId"_L1)) {
|
||||||
|
country_code_ = object_user["countryCode"_L1].toString();
|
||||||
|
user_id_ = object_user["userId"_L1].toInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
|
||||||
|
s.beginGroup(settings_group_);
|
||||||
|
s.setValue(kTokenType, token_type_);
|
||||||
|
s.setValue(kAccessToken, access_token_);
|
||||||
|
s.setValue(kLoginTime, login_time_);
|
||||||
|
|
||||||
|
if (refresh_token_.isEmpty()) {
|
||||||
|
s.remove(kRefreshToken);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s.setValue(kRefreshToken, refresh_token_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expires_in_ == 0) {
|
||||||
|
s.remove(kExpiresIn);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s.setValue(kExpiresIn, expires_in_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (country_code_.isEmpty()) {
|
||||||
|
s.remove(kCountryCode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s.setValue(kCountryCode, country_code_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_id_ == 0) {
|
||||||
|
s.remove(kUserId);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s.setValue(kUserId, user_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
StartRefreshLoginTimer();
|
||||||
|
|
||||||
|
qLog(Debug) << "OAuthenticator: Authentication was successful, login expires in" << expires_in_;
|
||||||
|
|
||||||
|
Q_EMIT AuthenticationFinished(true);
|
||||||
|
|
||||||
|
}
|
||||||
127
src/core/oauthenticator.h
Normal file
127
src/core/oauthenticator.h
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2022-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef OAUTHENTICATOR_H
|
||||||
|
#define OAUTHENTICATOR_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
#include <QSslError>
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
|
||||||
|
class QTimer;
|
||||||
|
class QNetworkReply;
|
||||||
|
class NetworkAccessManager;
|
||||||
|
class LocalRedirectServer;
|
||||||
|
|
||||||
|
class OAuthenticator : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit OAuthenticator(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||||
|
~OAuthenticator() override;
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
Authorization_Code,
|
||||||
|
Client_Credentials
|
||||||
|
};
|
||||||
|
|
||||||
|
void set_settings_group(const QString &settings_group);
|
||||||
|
void set_type(const Type type);
|
||||||
|
void set_authorize_url(const QUrl &auth_url);
|
||||||
|
void set_redirect_url(const QUrl &redirect_url);
|
||||||
|
void set_access_token_url(const QUrl &access_token_url);
|
||||||
|
void set_client_id(const QString &client_id);
|
||||||
|
void set_client_secret(const QString &client_secret);
|
||||||
|
void set_scope(const QString &scope);
|
||||||
|
void set_use_local_redirect_server(const bool use_local_redirect_server);
|
||||||
|
void set_random_port(const bool random_port);
|
||||||
|
|
||||||
|
QString token_type() const { return token_type_; }
|
||||||
|
QString access_token() const { return access_token_; }
|
||||||
|
qint64 expires_in() const { return expires_in_; }
|
||||||
|
QString country_code() const { return country_code_; }
|
||||||
|
quint64 user_id() const { return user_id_; }
|
||||||
|
bool authenticated() const { return !token_type_.isEmpty() && !access_token_.isEmpty(); }
|
||||||
|
|
||||||
|
QByteArray authorization_header() const;
|
||||||
|
|
||||||
|
void Authenticate();
|
||||||
|
void ClearSession();
|
||||||
|
void LoadSession();
|
||||||
|
void ExternalAuthorizationUrlReceived(const QUrl &request_url);
|
||||||
|
|
||||||
|
private:
|
||||||
|
using Param = QPair<QString, QString>;
|
||||||
|
using ParamList = QList<Param>;
|
||||||
|
|
||||||
|
QString GrantType() const;
|
||||||
|
void StartRefreshLoginTimer();
|
||||||
|
QNetworkReply *CreateRequest(const ParamList ¶ms);
|
||||||
|
void RequestAccessToken(const QString &code = QString(), const QUrl &redirect_url = QUrl());
|
||||||
|
void RenewAccessToken();
|
||||||
|
void AuthorizationUrlReceived(const QUrl &request_url, const QUrl &redirect_url);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void Error(const QString &error);
|
||||||
|
void AuthenticationFinished(const bool success, const QString &error = QString());
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void RedirectArrived();
|
||||||
|
void HandleLoginSSLErrors(const QList<QSslError> &ssl_errors);
|
||||||
|
void AccessTokenRequestFinished(QNetworkReply *reply);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const SharedPtr<NetworkAccessManager> network_;
|
||||||
|
QScopedPointer<LocalRedirectServer, QScopedPointerDeleteLater> local_redirect_server_;
|
||||||
|
QTimer *timer_refresh_login_;
|
||||||
|
|
||||||
|
QString settings_group_;
|
||||||
|
Type type_;
|
||||||
|
QUrl authorize_url_;
|
||||||
|
QUrl redirect_url_;
|
||||||
|
QUrl access_token_url_;
|
||||||
|
QString client_id_;
|
||||||
|
QString client_secret_;
|
||||||
|
QString scope_;
|
||||||
|
bool use_local_redirect_server_;
|
||||||
|
bool random_port_;
|
||||||
|
|
||||||
|
QString code_verifier_;
|
||||||
|
QString code_challenge_;
|
||||||
|
|
||||||
|
QString token_type_;
|
||||||
|
QString access_token_;
|
||||||
|
QString refresh_token_;
|
||||||
|
qint64 expires_in_;
|
||||||
|
qint64 login_time_;
|
||||||
|
QString country_code_;
|
||||||
|
quint64 user_id_;
|
||||||
|
|
||||||
|
QList<QNetworkReply*> replies_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // OAUTHENTICATOR_H
|
||||||
Reference in New Issue
Block a user