GH-4071 handle network errors when logging in with MSA as 'soft'

This makes the tokens not expire when such errors happen.

Only applies to MSA, not the XBox and Mojang steps afterwards.
Further testing and improvements are still needed.
This commit is contained in:
Petr Mrázek 2021-11-28 18:42:01 +01:00
parent 0e31f77468
commit 285188ea53
17 changed files with 724 additions and 1218 deletions

View File

@ -244,8 +244,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
}
case StatusColumn: {
auto isActive = account->isActive();
return isActive ? "Working" : "Ready";
if(account->isActive()) {
return tr("Working", "Account status");
}
if(account->isExpired()) {
return tr("Expired", "Account status");
}
return tr("Ready", "Account status");
}
case ProfileNameColumn: {

View File

@ -34,6 +34,11 @@
#include "flows/MojangRefresh.h"
#include "flows/MojangLogin.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
m_internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
}
MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) {
MinecraftAccountPtr account(new MinecraftAccount());
if(account->data.resumeStateFromV2(json)) {
@ -52,7 +57,7 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
{
MinecraftAccountPtr account(new MinecraftAccount());
MinecraftAccountPtr account = new MinecraftAccount();
account->data.type = AccountType::Mojang;
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
@ -91,6 +96,23 @@ AccountStatus MinecraftAccount::accountStatus() const {
}
}
bool MinecraftAccount::isExpired() const {
switch(data.type) {
case AccountType::Mojang: {
return data.accessToken().isEmpty();
}
break;
case AccountType::MSA: {
return data.msaToken.validity == Katabasis::Validity::None;
}
break;
default: {
return true;
}
}
}
QPixmap MinecraftAccount::getFace() const {
QPixmap skinTexture;
if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {

View File

@ -72,7 +72,7 @@ public: /* construction */
explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete;
//! Default constructor
explicit MinecraftAccount(QObject *parent = 0) : QObject(parent) {};
explicit MinecraftAccount(QObject *parent = 0);
static MinecraftAccountPtr createFromUsername(const QString &username);
@ -97,6 +97,10 @@ public: /* manipulation */
shared_qobject_ptr<AccountTask> refresh(AuthSessionPtr session);
public: /* queries */
QString internalId() const {
return m_internalId;
}
QString accountDisplayString() const {
return data.accountDisplayString();
}
@ -119,6 +123,8 @@ public: /* queries */
bool isActive() const;
bool isExpired() const;
bool canMigrate() const {
return data.canMigrateToMSA;
}
@ -168,6 +174,7 @@ signals:
// TODO: better signalling for the various possible state changes - especially errors
protected: /* variables */
QString m_internalId;
AccountData data;
// current task we are executing here

View File

@ -18,7 +18,7 @@
#include <Application.h>
using OAuth2 = Katabasis::OAuth2;
using OAuth2 = Katabasis::DeviceFlow;
using Activity = Katabasis::Activity;
AuthContext::AuthContext(AccountData * data, QObject *parent) :
@ -50,21 +50,17 @@ void AuthContext::initMSA() {
return;
}
Katabasis::OAuth2::Options opts;
OAuth2::Options opts;
opts.scope = "XboxLive.signin offline_access";
opts.clientIdentifier = APPLICATION->msaClientId();
opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
// FIXME: OAuth2 is not aware of our fancy shared pointers
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get());
m_oauth2->setGrantFlow(Katabasis::OAuth2::GrantFlowDevice);
connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed);
connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded);
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged);
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
}
void AuthContext::initMojang() {
@ -78,7 +74,7 @@ void AuthContext::initMojang() {
}
void AuthContext::onMojangSucceeded() {
doEntitlements();
doMinecraftProfile();
}
@ -89,50 +85,56 @@ void AuthContext::onMojangFailed() {
changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed."));
}
/*
bool AuthContext::signOut() {
if(isBusy()) {
return false;
}
start();
beginActivity(Activity::LoggingOut);
m_oauth2->unlink();
m_account = AccountData();
finishActivity();
return true;
}
*/
void AuthContext::onOAuthLinkingFailed() {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
}
void AuthContext::onOAuthLinkingSucceeded() {
emit hideVerificationUriAndCode();
auto *o2t = qobject_cast<OAuth2 *>(sender());
if (!o2t->linked()) {
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."));
return;
}
QVariantMap extraTokens = o2t->extraTokens();
#ifndef NDEBUG
if (!extraTokens.isEmpty()) {
qDebug() << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qDebug() << "\t" << key << ":" << extraTokens.value(key);
}
}
#endif
doUserAuth();
}
void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
// respond to activity change here
switch(activity) {
case Katabasis::Activity::Idle:
case Katabasis::Activity::LoggingIn:
case Katabasis::Activity::Refreshing:
case Katabasis::Activity::LoggingOut: {
// We asked it to do something, it's doing it. Nothing to act upon.
return;
}
case Katabasis::Activity::Succeeded: {
// Succeeded or did not invalidate tokens
emit hideVerificationUriAndCode();
if (!m_oauth2->linked()) {
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."));
return;
}
QVariantMap extraTokens = m_oauth2->extraTokens();
#ifndef NDEBUG
if (!extraTokens.isEmpty()) {
qDebug() << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qDebug() << "\t" << key << ":" << extraTokens.value(key);
}
}
#endif
doUserAuth();
return;
}
case Katabasis::Activity::FailedSoft: {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error."));
return;
}
case Katabasis::Activity::FailedGone:
case Katabasis::Activity::FailedHard: {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
return;
}
default: {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
return;
}
}
}
void AuthContext::doUserAuth() {
@ -226,7 +228,7 @@ void AuthContext::doSTSAuthMinecraft() {
void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
if(error == QNetworkReply::AuthenticationRequiredError) {
QJsonParseError jsonError;
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
@ -543,6 +545,10 @@ void AuthContext::onMinecraftProfileDone(
#endif
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
if(m_data->type == AccountType::Mojang) {
m_data->minecraftEntitlement.canPlayMinecraft = false;
m_data->minecraftEntitlement.ownsMinecraft = false;
}
m_data->minecraftProfile = MinecraftProfile();
succeed();
return;
@ -560,6 +566,9 @@ void AuthContext::onMinecraftProfileDone(
}
if(m_data->type == AccountType::Mojang) {
auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
m_data->minecraftEntitlement.ownsMinecraft = validProfile;
doMigrationEligibilityCheck();
}
else {

View File

@ -7,7 +7,7 @@
#include <QNetworkReply>
#include <QImage>
#include <katabasis/OAuth2.h>
#include <katabasis/DeviceFlow.h>
#include "Yggdrasil.h"
#include "../AccountData.h"
#include "../AccountTask.h"
@ -35,9 +35,6 @@ signals:
private slots:
// OAuth-specific callbacks
void onOAuthLinkingSucceeded();
void onOAuthLinkingFailed();
void onOAuthActivityChanged(Katabasis::Activity activity);
// Yggdrasil specific callbacks
@ -87,7 +84,7 @@ protected:
void clearTokens();
protected:
Katabasis::OAuth2 *m_oauth2 = nullptr;
Katabasis::DeviceFlow *m_oauth2 = nullptr;
Yggdrasil *m_yggdrasil = nullptr;
int m_requestsDone = 0;

View File

@ -17,7 +17,6 @@ void MSAInteractive::executeTask() {
m_oauth2->setExtraRequestParams(extraOpts);
beginActivity(Katabasis::Activity::LoggingIn);
m_oauth2->unlink();
*m_data = AccountData();
m_oauth2->link();
m_oauth2->login();
}

View File

@ -19,7 +19,6 @@
#include <QDrag>
#include <QPainter>
#include "VersionListView.h"
#include "Common.h"
VersionListView::VersionListView(QWidget *parent)
:QTreeView ( parent )

View File

@ -27,20 +27,18 @@ set(CMAKE_C_STANDARD 11)
find_package(Qt5 COMPONENTS Core Network REQUIRED)
set( katabasis_PRIVATE
src/OAuth2.cpp
src/DeviceFlow.cpp
src/JsonResponse.cpp
src/JsonResponse.h
src/PollServer.cpp
src/Reply.cpp
src/ReplyServer.cpp
)
set( katabasis_PUBLIC
include/katabasis/OAuth2.h
include/katabasis/DeviceFlow.h
include/katabasis/Globals.h
include/katabasis/PollServer.h
include/katabasis/Reply.h
include/katabasis/ReplyServer.h
include/katabasis/RequestParameter.h
)

View File

@ -10,7 +10,11 @@ enum class Activity {
Idle,
LoggingIn,
LoggingOut,
Refreshing
Refreshing,
FailedSoft, //!< soft failure. this generally means the user auth details haven't been invalidated
FailedHard, //!< hard failure. auth is invalid
FailedGone, //!< hard failure. auth is invalid, and the account no longer exists
Succeeded
};
enum class Validity {

View File

@ -0,0 +1,150 @@
#pragma once
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QPair>
#include "Reply.h"
#include "RequestParameter.h"
#include "Bits.h"
namespace Katabasis {
class ReplyServer;
class PollServer;
/// Simple OAuth2 Device Flow authenticator.
class DeviceFlow: public QObject
{
Q_OBJECT
public:
Q_ENUMS(GrantFlow)
public:
struct Options {
QString userAgent = QStringLiteral("Katabasis/1.0");
QString responseType = QStringLiteral("code");
QString scope;
QString clientIdentifier;
QString clientSecret;
QUrl authorizationUrl;
QUrl accessTokenUrl;
};
public:
/// Are we authenticated?
bool linked();
/// Authentication token.
QString token();
/// Provider-specific extra tokens, available after a successful authentication
QVariantMap extraTokens();
public:
// TODO: put in `Options`
/// User-defined extra parameters to append to request URL
QVariantMap extraRequestParams();
void setExtraRequestParams(const QVariantMap &value);
// TODO: split up the class into multiple, each implementing one OAuth2 flow
/// Grant type (if non-standard)
QString grantType();
void setGrantType(const QString &value);
public:
/// Constructor.
/// @param parent Parent object.
explicit DeviceFlow(Options & opts, Token & token, QObject *parent = 0, QNetworkAccessManager *manager = 0);
/// Get refresh token.
QString refreshToken();
/// Get token expiration time
QDateTime expires();
public slots:
/// Authenticate.
void login();
/// De-authenticate.
void logout();
/// Refresh token.
bool refresh();
/// Handle situation where reply server has opted to close its connection
void serverHasClosed(bool paramsfound = false);
signals:
/// Emitted when client needs to open a web browser window, with the given URL.
void openBrowser(const QUrl &url);
/// Emitted when client can close the browser window.
void closeBrowser();
/// Emitted when client needs to show a verification uri and user code
void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn);
/// Emitted when the internal state changes
void activityChanged(Activity activity);
public slots:
/// Handle verification response.
void onVerificationReceived(QMap<QString, QString>);
protected slots:
/// Handle completion of a Device Authorization Request
void onDeviceAuthReplyFinished();
/// Handle completion of a refresh request.
void onRefreshFinished();
/// Handle failure of a refresh request.
void onRefreshError(QNetworkReply::NetworkError error, QNetworkReply *reply);
protected:
/// Set refresh token.
void setRefreshToken(const QString &v);
/// Set token expiration time.
void setExpires(QDateTime v);
/// Start polling authorization server
void startPollServer(const QVariantMap &params, int expiresIn);
/// Set authentication token.
void setToken(const QString &v);
/// Set the linked state
void setLinked(bool v);
/// Set extra tokens found in OAuth response
void setExtraTokens(QVariantMap extraTokens);
/// Set local poll server
void setPollServer(PollServer *server);
PollServer * pollServer() const;
void updateActivity(Activity activity);
protected:
Options options_;
QVariantMap extraReqParams_;
QNetworkAccessManager *manager_ = nullptr;
ReplyList timedReplies_;
QString grantType_;
protected:
Token &token_;
private:
PollServer *pollServer_ = nullptr;
Activity activity_ = Activity::Idle;
};
}

View File

@ -1,233 +0,0 @@
#pragma once
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QPair>
#include "Reply.h"
#include "RequestParameter.h"
#include "Bits.h"
namespace Katabasis {
class ReplyServer;
class PollServer;
/*
* FIXME: this is not as simple as it should be. it squishes 4 different grant flows into one big ball of mud
* This serves no practical purpose and simply makes the code less readable / maintainable.
*
* Therefore: Split this into the 4 different OAuth2 flows that people can use as authentication steps. Write tests/examples for all of them.
*/
/// Simple OAuth2 authenticator.
class OAuth2: public QObject
{
Q_OBJECT
public:
Q_ENUMS(GrantFlow)
public:
struct Options {
QString userAgent = QStringLiteral("Katabasis/1.0");
QString redirectionUrl = QStringLiteral("http://localhost:%1");
QString responseType = QStringLiteral("code");
QString scope;
QString clientIdentifier;
QString clientSecret;
QUrl authorizationUrl;
QUrl accessTokenUrl;
QVector<quint16> listenerPorts = { 0 };
};
/// Authorization flow types.
enum GrantFlow {
GrantFlowAuthorizationCode, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
GrantFlowImplicit, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.2
GrantFlowResourceOwnerPasswordCredentials,
GrantFlowDevice ///< @see https://tools.ietf.org/html/rfc8628#section-1
};
/// Authorization flow.
GrantFlow grantFlow();
void setGrantFlow(GrantFlow value);
public:
/// Are we authenticated?
bool linked();
/// Authentication token.
QString token();
/// Provider-specific extra tokens, available after a successful authentication
QVariantMap extraTokens();
/// Page content on local host after successful oauth.
/// Provide it in case you do not want to close the browser, but display something
QByteArray replyContent() const;
void setReplyContent(const QByteArray &value);
public:
// TODO: remove
/// Resource owner username.
/// instances with the same (username, password) share the same "linked" and "token" properties.
QString username();
void setUsername(const QString &value);
// TODO: remove
/// Resource owner password.
/// instances with the same (username, password) share the same "linked" and "token" properties.
QString password();
void setPassword(const QString &value);
// TODO: remove
/// API key.
QString apiKey();
void setApiKey(const QString &value);
// TODO: remove
/// Allow ignoring SSL errors?
/// E.g. SurveyMonkey fails on Mac due to SSL error. Ignoring the error circumvents the problem
bool ignoreSslErrors();
void setIgnoreSslErrors(bool ignoreSslErrors);
// TODO: put in `Options`
/// User-defined extra parameters to append to request URL
QVariantMap extraRequestParams();
void setExtraRequestParams(const QVariantMap &value);
// TODO: split up the class into multiple, each implementing one OAuth2 flow
/// Grant type (if non-standard)
QString grantType();
void setGrantType(const QString &value);
public:
/// Constructor.
/// @param parent Parent object.
explicit OAuth2(Options & opts, Token & token, QObject *parent = 0, QNetworkAccessManager *manager = 0);
/// Get refresh token.
QString refreshToken();
/// Get token expiration time
QDateTime expires();
public slots:
/// Authenticate.
virtual void link();
/// De-authenticate.
virtual void unlink();
/// Refresh token.
bool refresh();
/// Handle situation where reply server has opted to close its connection
void serverHasClosed(bool paramsfound = false);
signals:
/// Emitted when a token refresh has been completed or failed.
void refreshFinished(QNetworkReply::NetworkError error);
/// Emitted when client needs to open a web browser window, with the given URL.
void openBrowser(const QUrl &url);
/// Emitted when client can close the browser window.
void closeBrowser();
/// Emitted when client needs to show a verification uri and user code
void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn);
/// Emitted when authentication/deauthentication succeeded.
void linkingSucceeded();
/// Emitted when authentication/deauthentication failed.
void linkingFailed();
void activityChanged(Activity activity);
public slots:
/// Handle verification response.
virtual void onVerificationReceived(QMap<QString, QString>);
protected slots:
/// Handle completion of a token request.
virtual void onTokenReplyFinished();
/// Handle failure of a token request.
virtual void onTokenReplyError(QNetworkReply::NetworkError error);
/// Handle completion of a refresh request.
virtual void onRefreshFinished();
/// Handle failure of a refresh request.
virtual void onRefreshError(QNetworkReply::NetworkError error);
/// Handle completion of a Device Authorization Request
virtual void onDeviceAuthReplyFinished();
protected:
/// Build HTTP request body.
QByteArray buildRequestBody(const QMap<QString, QString> &parameters);
/// Set refresh token.
void setRefreshToken(const QString &v);
/// Set token expiration time.
void setExpires(QDateTime v);
/// Start polling authorization server
void startPollServer(const QVariantMap &params, int expiresIn);
/// Set authentication token.
void setToken(const QString &v);
/// Set the linked state
void setLinked(bool v);
/// Set extra tokens found in OAuth response
void setExtraTokens(QVariantMap extraTokens);
/// Set local reply server
void setReplyServer(ReplyServer *server);
ReplyServer * replyServer() const;
/// Set local poll server
void setPollServer(PollServer *server);
PollServer * pollServer() const;
void updateActivity(Activity activity);
protected:
QString username_;
QString password_;
Options options_;
QVariantMap extraReqParams_;
QString apiKey_;
QNetworkAccessManager *manager_ = nullptr;
ReplyList timedReplies_;
GrantFlow grantFlow_;
QString grantType_;
protected:
QString redirectUri_;
Token &token_;
// this should be part of the reply server impl
QByteArray replyContent_;
private:
ReplyServer *replyServer_ = nullptr;
PollServer *pollServer_ = nullptr;
Activity activity_ = Activity::Idle;
};
}

View File

@ -9,12 +9,14 @@
namespace Katabasis {
constexpr int defaultTimeout = 30 * 1000;
/// A network request/reply pair that can time out.
class Reply: public QTimer {
Q_OBJECT
public:
Reply(QNetworkReply *reply, int timeOut = 60 * 1000, QObject *parent = 0);
Reply(QNetworkReply *reply, int timeOut = defaultTimeout, QObject *parent = 0);
signals:
void error(QNetworkReply::NetworkError);
@ -25,6 +27,7 @@ public slots:
public:
QNetworkReply *reply;
bool timedOut = false;
};
/// List of O2Replies.
@ -37,7 +40,7 @@ public:
virtual ~ReplyList();
/// Create a new O2Reply from a QNetworkReply, and add it to this list.
void add(QNetworkReply *reply);
void add(QNetworkReply *reply, int timeOut = defaultTimeout);
/// Add an O2Reply to the list, while taking ownership of it.
void add(Reply *reply);

View File

@ -1,53 +0,0 @@
#pragma once
#include <QTcpServer>
#include <QMap>
#include <QByteArray>
#include <QString>
namespace Katabasis {
/// HTTP server to process authentication response.
class ReplyServer: public QTcpServer {
Q_OBJECT
public:
explicit ReplyServer(QObject *parent = 0);
/// Page content on local host after successful oauth - in case you do not want to close the browser, but display something
Q_PROPERTY(QByteArray replyContent READ replyContent WRITE setReplyContent)
QByteArray replyContent();
void setReplyContent(const QByteArray &value);
/// Seconds to keep listening *after* first response for a callback with token content
Q_PROPERTY(int timeout READ timeout WRITE setTimeout)
int timeout();
void setTimeout(int timeout);
/// Maximum number of callback tries to accept, in case some don't have token content (favicons, etc.)
Q_PROPERTY(int callbackTries READ callbackTries WRITE setCallbackTries)
int callbackTries();
void setCallbackTries(int maxtries);
QString uniqueState();
void setUniqueState(const QString &state);
signals:
void verificationReceived(QMap<QString, QString>);
void serverClosed(bool); // whether it has found parameters
public slots:
void onIncomingConnection();
void onBytesReady();
QMap<QString, QString> parseQueryParams(QByteArray *data);
void closeServer(QTcpSocket *socket = 0, bool hasparameters = false);
protected:
QByteArray replyContent_;
int timeout_;
int maxtries_;
int tries_;
QString uniqueState_;
};
}

View File

@ -0,0 +1,450 @@
#include <QList>
#include <QPair>
#include <QDebug>
#include <QTcpServer>
#include <QMap>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QDateTime>
#include <QCryptographicHash>
#include <QTimer>
#include <QVariantMap>
#include <QUuid>
#include <QDataStream>
#include <QUrlQuery>
#include "katabasis/DeviceFlow.h"
#include "katabasis/PollServer.h"
#include "katabasis/Globals.h"
#include "JsonResponse.h"
namespace {
// ref: https://tools.ietf.org/html/rfc8628#section-3.2
// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both.
bool hasMandatoryDeviceAuthParams(const QVariantMap& params)
{
if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE))
return false;
if (!params.contains(Katabasis::OAUTH2_USER_CODE))
return false;
if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || params.contains(Katabasis::OAUTH2_VERIFICATION_URL)))
return false;
if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN))
return false;
return true;
}
QByteArray createQueryParameters(const QList<Katabasis::RequestParameter> &parameters) {
QByteArray ret;
bool first = true;
for( auto & h: parameters) {
if (first) {
first = false;
} else {
ret.append("&");
}
ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value));
}
return ret;
}
}
namespace Katabasis {
DeviceFlow::DeviceFlow(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) {
manager_ = manager ? manager : new QNetworkAccessManager(this);
qRegisterMetaType<QNetworkReply::NetworkError>("QNetworkReply::NetworkError");
options_ = opts;
}
bool DeviceFlow::linked() {
return token_.validity != Validity::None;
}
void DeviceFlow::setLinked(bool v) {
qDebug() << "DeviceFlow::setLinked:" << (v? "true": "false");
token_.validity = v ? Validity::Certain : Validity::None;
}
void DeviceFlow::updateActivity(Activity activity)
{
if(activity_ == activity) {
return;
}
activity_ = activity;
switch(activity) {
case Katabasis::Activity::Idle:
case Katabasis::Activity::LoggingIn:
case Katabasis::Activity::LoggingOut:
case Katabasis::Activity::Refreshing:
// non-terminal states...
break;
case Katabasis::Activity::FailedSoft:
// terminal state, tokens did not change
break;
case Katabasis::Activity::FailedHard:
case Katabasis::Activity::FailedGone:
// terminal state, tokens are invalid
token_ = Token();
break;
case Katabasis::Activity::Succeeded:
setLinked(true);
break;
}
emit activityChanged(activity_);
}
QString DeviceFlow::token() {
return token_.token;
}
void DeviceFlow::setToken(const QString &v) {
token_.token = v;
}
QVariantMap DeviceFlow::extraTokens() {
return token_.extra;
}
void DeviceFlow::setExtraTokens(QVariantMap extraTokens) {
token_.extra = extraTokens;
}
void DeviceFlow::setPollServer(PollServer *server)
{
if (pollServer_)
pollServer_->deleteLater();
pollServer_ = server;
}
PollServer *DeviceFlow::pollServer() const
{
return pollServer_;
}
QVariantMap DeviceFlow::extraRequestParams()
{
return extraReqParams_;
}
void DeviceFlow::setExtraRequestParams(const QVariantMap &value)
{
extraReqParams_ = value;
}
QString DeviceFlow::grantType()
{
if (!grantType_.isEmpty())
return grantType_;
return OAUTH2_GRANT_TYPE_DEVICE;
}
void DeviceFlow::setGrantType(const QString &value)
{
grantType_ = value;
}
// First get the URL and token to display to the user
void DeviceFlow::login() {
qDebug() << "DeviceFlow::link";
updateActivity(Activity::LoggingIn);
setLinked(false);
setToken("");
setExtraTokens(QVariantMap());
setRefreshToken(QString());
setExpires(QDateTime());
QList<RequestParameter> parameters;
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8()));
QByteArray payload = createQueryParameters(parameters);
QUrl url(options_.authorizationUrl);
QNetworkRequest deviceRequest(url);
deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply *tokenReply = manager_->post(deviceRequest, payload);
connect(tokenReply, &QNetworkReply::finished, this, &DeviceFlow::onDeviceAuthReplyFinished, Qt::QueuedConnection);
}
// Then, once we get them, present them to the user
void DeviceFlow::onDeviceAuthReplyFinished()
{
qDebug() << "DeviceFlow::onDeviceAuthReplyFinished";
QNetworkReply *tokenReply = qobject_cast<QNetworkReply *>(sender());
if (!tokenReply)
{
qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: reply is null";
return;
}
if (tokenReply->error() == QNetworkReply::NoError) {
QByteArray replyData = tokenReply->readAll();
// Dump replyData
// SENSITIVE DATA in RelWithDebInfo or Debug builds
//qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: replyData\n";
//qDebug() << QString( replyData );
QVariantMap params = parseJsonResponse(replyData);
// Dump tokens
qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Tokens returned:\n";
foreach (QString key, params.keys()) {
// SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first
qDebug() << key << ": "<< params.value( key ).toString();
}
// Check for mandatory parameters
if (hasMandatoryDeviceAuthParams(params)) {
qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Device auth request response";
const QString userCode = params.take(OAUTH2_USER_CODE).toString();
QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl();
if (uri.isEmpty())
uri = params.take(OAUTH2_VERIFICATION_URL).toUrl();
if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE))
emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl());
bool ok = false;
int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok);
if (!ok) {
qWarning() << "DeviceFlow::startPollServer: No expired_in parameter";
updateActivity(Activity::FailedHard);
return;
}
emit showVerificationUriAndCode(uri, userCode, expiresIn);
startPollServer(params, expiresIn);
} else {
qWarning() << "DeviceFlow::onDeviceAuthReplyFinished: Mandatory parameters missing from response";
updateActivity(Activity::FailedHard);
}
}
tokenReply->deleteLater();
}
// Spin up polling for the user completing the login flow out of band
void DeviceFlow::startPollServer(const QVariantMap &params, int expiresIn)
{
qDebug() << "DeviceFlow::startPollServer: device_ and user_code expires in" << expiresIn << "seconds";
QUrl url(options_.accessTokenUrl);
QNetworkRequest authRequest(url);
authRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString();
const QString grantType = grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_;
QList<RequestParameter> parameters;
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
if ( !options_.clientSecret.isEmpty() ) {
parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8()));
}
parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8()));
parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8()));
QByteArray payload = createQueryParameters(parameters);
PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this);
if (params.contains(OAUTH2_INTERVAL)) {
bool ok = false;
int interval = params[OAUTH2_INTERVAL].toInt(&ok);
if (ok) {
pollServer->setInterval(interval);
}
}
connect(pollServer, &PollServer::verificationReceived, this, &DeviceFlow::onVerificationReceived);
connect(pollServer, &PollServer::serverClosed, this, &DeviceFlow::serverHasClosed);
setPollServer(pollServer);
pollServer->startPolling();
}
// Once the user completes the flow, update the internal state and report it to observers
void DeviceFlow::onVerificationReceived(const QMap<QString, QString> response) {
qDebug() << "DeviceFlow::onVerificationReceived: Emitting closeBrowser()";
emit closeBrowser();
if (response.contains("error")) {
qWarning() << "DeviceFlow::onVerificationReceived: Verification failed:" << response;
updateActivity(Activity::FailedHard);
return;
}
// Check for mandatory tokens
if (response.contains(OAUTH2_ACCESS_TOKEN)) {
qDebug() << "DeviceFlow::onVerificationReceived: Access token returned for implicit or device flow";
setToken(response.value(OAUTH2_ACCESS_TOKEN));
if (response.contains(OAUTH2_EXPIRES_IN)) {
bool ok = false;
int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok);
if (ok) {
qDebug() << "DeviceFlow::onVerificationReceived: Token expires in" << expiresIn << "seconds";
setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn));
}
}
if (response.contains(OAUTH2_REFRESH_TOKEN)) {
setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN));
}
updateActivity(Activity::Succeeded);
} else {
qWarning() << "DeviceFlow::onVerificationReceived: Access token missing from response for implicit or device flow";
updateActivity(Activity::FailedHard);
}
}
// Or if the flow fails or the polling times out, update the internal state with error and report it to observers
void DeviceFlow::serverHasClosed(bool paramsfound)
{
if ( !paramsfound ) {
// server has probably timed out after receiving first response
updateActivity(Activity::FailedHard);
}
// poll server is not re-used for later auth requests
setPollServer(NULL);
}
void DeviceFlow::logout() {
qDebug() << "DeviceFlow::unlink";
updateActivity(Activity::LoggingOut);
// FIXME: implement logout flows... if they exist
token_ = Token();
updateActivity(Activity::FailedHard);
}
QDateTime DeviceFlow::expires() {
return token_.notAfter;
}
void DeviceFlow::setExpires(QDateTime v) {
token_.notAfter = v;
}
QString DeviceFlow::refreshToken() {
return token_.refresh_token;
}
void DeviceFlow::setRefreshToken(const QString &v) {
#ifndef NDEBUG
qDebug() << "DeviceFlow::setRefreshToken" << v << "...";
#endif
token_.refresh_token = v;
}
namespace {
QByteArray buildRequestBody(const QMap<QString, QString> &parameters) {
QByteArray body;
bool first = true;
foreach (QString key, parameters.keys()) {
if (first) {
first = false;
} else {
body.append("&");
}
QString value = parameters.value(key);
body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value));
}
return body;
}
}
bool DeviceFlow::refresh() {
qDebug() << "DeviceFlow::refresh: Token: ..." << refreshToken().right(7);
updateActivity(Activity::Refreshing);
if (refreshToken().isEmpty()) {
qWarning() << "DeviceFlow::refresh: No refresh token";
onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr);
return false;
}
if (options_.accessTokenUrl.isEmpty()) {
qWarning() << "DeviceFlow::refresh: Refresh token URL not set";
onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr);
return false;
}
QNetworkRequest refreshRequest(options_.accessTokenUrl);
refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM);
QMap<QString, QString> parameters;
parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier);
if ( !options_.clientSecret.isEmpty() ) {
parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret);
}
parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken());
parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN);
QByteArray data = buildRequestBody(parameters);
QNetworkReply *refreshReply = manager_->post(refreshRequest, data);
timedReplies_.add(refreshReply);
connect(refreshReply, &QNetworkReply::finished, this, &DeviceFlow::onRefreshFinished, Qt::QueuedConnection);
return true;
}
void DeviceFlow::onRefreshFinished() {
QNetworkReply *refreshReply = qobject_cast<QNetworkReply *>(sender());
auto networkError = refreshReply->error();
if (networkError == QNetworkReply::NoError) {
QByteArray reply = refreshReply->readAll();
QVariantMap tokens = parseJsonResponse(reply);
setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString());
setExpires(QDateTime::currentDateTimeUtc().addSecs(tokens.value(OAUTH2_EXPIRES_IN).toInt()));
QString refreshToken = tokens.value(OAUTH2_REFRESH_TOKEN).toString();
if(!refreshToken.isEmpty()) {
setRefreshToken(refreshToken);
}
else {
qDebug() << "No new refresh token. Keep the old one.";
}
timedReplies_.remove(refreshReply);
refreshReply->deleteLater();
updateActivity(Activity::Succeeded);
qDebug() << "New token expires in" << expires() << "seconds";
} else {
// FIXME: differentiate the error more here
onRefreshError(networkError, refreshReply);
}
}
void DeviceFlow::onRefreshError(QNetworkReply::NetworkError error, QNetworkReply *refreshReply) {
QString errorString = "No Reply";
if(refreshReply) {
timedReplies_.remove(refreshReply);
errorString = refreshReply->errorString();
}
switch (error)
{
// used for invalid credentials and similar errors. Fall through.
case QNetworkReply::AuthenticationRequiredError:
case QNetworkReply::ContentAccessDenied:
case QNetworkReply::ContentOperationNotPermittedError:
updateActivity(Activity::FailedHard);
break;
case QNetworkReply::ContentGoneError: {
updateActivity(Activity::FailedGone);
break;
}
case QNetworkReply::TimeoutError:
case QNetworkReply::OperationCanceledError:
case QNetworkReply::SslHandshakeFailedError:
default:
updateActivity(Activity::FailedSoft);
return;
}
if(refreshReply) {
refreshReply->deleteLater();
}
qDebug() << "DeviceFlow::onRefreshFinished: Error" << (int)error << " - " << errorString;
}
}

View File

@ -1,672 +0,0 @@
#include <QList>
#include <QPair>
#include <QDebug>
#include <QTcpServer>
#include <QMap>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QDateTime>
#include <QCryptographicHash>
#include <QTimer>
#include <QVariantMap>
#include <QUuid>
#include <QDataStream>
#include <QUrlQuery>
#include "katabasis/OAuth2.h"
#include "katabasis/PollServer.h"
#include "katabasis/ReplyServer.h"
#include "katabasis/Globals.h"
#include "JsonResponse.h"
namespace {
// ref: https://tools.ietf.org/html/rfc8628#section-3.2
// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both.
bool hasMandatoryDeviceAuthParams(const QVariantMap& params)
{
if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE))
return false;
if (!params.contains(Katabasis::OAUTH2_USER_CODE))
return false;
if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || params.contains(Katabasis::OAUTH2_VERIFICATION_URL)))
return false;
if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN))
return false;
return true;
}
QByteArray createQueryParameters(const QList<Katabasis::RequestParameter> &parameters) {
QByteArray ret;
bool first = true;
for( auto & h: parameters) {
if (first) {
first = false;
} else {
ret.append("&");
}
ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value));
}
return ret;
}
}
namespace Katabasis {
OAuth2::OAuth2(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) {
manager_ = manager ? manager : new QNetworkAccessManager(this);
grantFlow_ = GrantFlowAuthorizationCode;
qRegisterMetaType<QNetworkReply::NetworkError>("QNetworkReply::NetworkError");
options_ = opts;
}
bool OAuth2::linked() {
return token_.validity != Validity::None;
}
void OAuth2::setLinked(bool v) {
qDebug() << "OAuth2::setLinked:" << (v? "true": "false");
token_.validity = v ? Validity::Certain : Validity::None;
}
QString OAuth2::token() {
return token_.token;
}
void OAuth2::setToken(const QString &v) {
token_.token = v;
}
QByteArray OAuth2::replyContent() const {
return replyContent_;
}
void OAuth2::setReplyContent(const QByteArray &value) {
replyContent_ = value;
if (replyServer_) {
replyServer_->setReplyContent(replyContent_);
}
}
QVariantMap OAuth2::extraTokens() {
return token_.extra;
}
void OAuth2::setExtraTokens(QVariantMap extraTokens) {
token_.extra = extraTokens;
}
void OAuth2::setReplyServer(ReplyServer * server)
{
delete replyServer_;
replyServer_ = server;
replyServer_->setReplyContent(replyContent_);
}
ReplyServer * OAuth2::replyServer() const
{
return replyServer_;
}
void OAuth2::setPollServer(PollServer *server)
{
if (pollServer_)
pollServer_->deleteLater();
pollServer_ = server;
}
PollServer *OAuth2::pollServer() const
{
return pollServer_;
}
OAuth2::GrantFlow OAuth2::grantFlow() {
return grantFlow_;
}
void OAuth2::setGrantFlow(OAuth2::GrantFlow value) {
grantFlow_ = value;
}
QString OAuth2::username() {
return username_;
}
void OAuth2::setUsername(const QString &value) {
username_ = value;
}
QString OAuth2::password() {
return password_;
}
void OAuth2::setPassword(const QString &value) {
password_ = value;
}
QVariantMap OAuth2::extraRequestParams()
{
return extraReqParams_;
}
void OAuth2::setExtraRequestParams(const QVariantMap &value)
{
extraReqParams_ = value;
}
QString OAuth2::grantType()
{
if (!grantType_.isEmpty())
return grantType_;
switch (grantFlow_) {
case GrantFlowAuthorizationCode:
return OAUTH2_GRANT_TYPE_CODE;
case GrantFlowImplicit:
return OAUTH2_GRANT_TYPE_TOKEN;
case GrantFlowResourceOwnerPasswordCredentials:
return OAUTH2_GRANT_TYPE_PASSWORD;
case GrantFlowDevice:
return OAUTH2_GRANT_TYPE_DEVICE;
}
return QString();
}
void OAuth2::setGrantType(const QString &value)
{
grantType_ = value;
}
void OAuth2::updateActivity(Activity activity)
{
if(activity_ != activity) {
activity_ = activity;
emit activityChanged(activity_);
}
}
void OAuth2::link() {
qDebug() << "OAuth2::link";
// Create the reply server if it doesn't exist
if(replyServer() == NULL) {
ReplyServer * replyServer = new ReplyServer(this);
connect(replyServer, &ReplyServer::verificationReceived, this, &OAuth2::onVerificationReceived);
connect(replyServer, &ReplyServer::serverClosed, this, &OAuth2::serverHasClosed);
setReplyServer(replyServer);
}
if (linked()) {
qDebug() << "OAuth2::link: Linked already";
emit linkingSucceeded();
return;
}
setLinked(false);
setToken("");
setExtraTokens(QVariantMap());
setRefreshToken(QString());
setExpires(QDateTime());
if (grantFlow_ == GrantFlowAuthorizationCode || grantFlow_ == GrantFlowImplicit) {
QString uniqueState = QUuid::createUuid().toString().remove(QRegExp("([^a-zA-Z0-9]|[-])"));
// FIXME: this should be part of a 'redirection handler' that would get injected into O2
{
quint16 foundPort = 0;
// Start listening to authentication replies
if (!replyServer()->isListening()) {
auto ports = options_.listenerPorts;
for(auto & port: ports) {
if (replyServer()->listen(QHostAddress::Any, port)) {
foundPort = replyServer()->serverPort();
qDebug() << "OAuth2::link: Reply server listening on port " << foundPort;
break;
}
}
if(foundPort == 0) {
qWarning() << "OAuth2::link: Reply server failed to start listening on any port out of " << ports;
emit linkingFailed();
return;
}
}
// Save redirect URI, as we have to reuse it when requesting the access token
redirectUri_ = options_.redirectionUrl.arg(foundPort);
replyServer()->setUniqueState(uniqueState);
}
// Assemble intial authentication URL
QUrl url(options_.authorizationUrl);
QUrlQuery query(url);
QList<QPair<QString, QString> > parameters;
query.addQueryItem(OAUTH2_RESPONSE_TYPE, (grantFlow_ == GrantFlowAuthorizationCode)? OAUTH2_GRANT_TYPE_CODE: OAUTH2_GRANT_TYPE_TOKEN);
query.addQueryItem(OAUTH2_CLIENT_ID, options_.clientIdentifier);
query.addQueryItem(OAUTH2_REDIRECT_URI, redirectUri_);
query.addQueryItem(OAUTH2_SCOPE, options_.scope.replace( " ", "+" ));
query.addQueryItem(OAUTH2_STATE, uniqueState);
if (!apiKey_.isEmpty()) {
query.addQueryItem(OAUTH2_API_KEY, apiKey_);
}
for(auto iter = extraReqParams_.begin(); iter != extraReqParams_.end(); iter++) {
query.addQueryItem(iter.key(), iter.value().toString());
}
url.setQuery(query);
// Show authentication URL with a web browser
qDebug() << "OAuth2::link: Emit openBrowser" << url.toString();
emit openBrowser(url);
updateActivity(Activity::LoggingIn);
} else if (grantFlow_ == GrantFlowResourceOwnerPasswordCredentials) {
QList<RequestParameter> parameters;
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
if ( !options_.clientSecret.isEmpty() ) {
parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8()));
}
parameters.append(RequestParameter(OAUTH2_USERNAME, username_.toUtf8()));
parameters.append(RequestParameter(OAUTH2_PASSWORD, password_.toUtf8()));
parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, OAUTH2_GRANT_TYPE_PASSWORD));
parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8()));
if ( !apiKey_.isEmpty() )
parameters.append(RequestParameter(OAUTH2_API_KEY, apiKey_.toUtf8()));
foreach (QString key, extraRequestParams().keys()) {
parameters.append(RequestParameter(key.toUtf8(), extraRequestParams().value(key).toByteArray()));
}
QByteArray payload = createQueryParameters(parameters);
qDebug() << "OAuth2::link: Sending token request for resource owner flow";
QUrl url(options_.accessTokenUrl);
QNetworkRequest tokenRequest(url);
tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply *tokenReply = manager_->post(tokenRequest, payload);
connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection);
connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
updateActivity(Activity::LoggingIn);
}
else if (grantFlow_ == GrantFlowDevice) {
QList<RequestParameter> parameters;
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8()));
QByteArray payload = createQueryParameters(parameters);
QUrl url(options_.authorizationUrl);
QNetworkRequest deviceRequest(url);
deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply *tokenReply = manager_->post(deviceRequest, payload);
connect(tokenReply, SIGNAL(finished()), this, SLOT(onDeviceAuthReplyFinished()), Qt::QueuedConnection);
connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
updateActivity(Activity::LoggingIn);
}
}
void OAuth2::unlink() {
qDebug() << "OAuth2::unlink";
updateActivity(Activity::LoggingOut);
// FIXME: implement logout flows... if they exist
token_ = Token();
updateActivity(Activity::Idle);
}
void OAuth2::onVerificationReceived(const QMap<QString, QString> response) {
qDebug() << "OAuth2::onVerificationReceived: Emitting closeBrowser()";
emit closeBrowser();
if (response.contains("error")) {
qWarning() << "OAuth2::onVerificationReceived: Verification failed:" << response;
emit linkingFailed();
updateActivity(Activity::Idle);
return;
}
if (grantFlow_ == GrantFlowAuthorizationCode) {
// NOTE: access code is temporary and should never be saved anywhere!
auto access_code = response.value(QString(OAUTH2_GRANT_TYPE_CODE));
// Exchange access code for access/refresh tokens
QString query;
if(!apiKey_.isEmpty())
query = QString("?" + QString(OAUTH2_API_KEY) + "=" + apiKey_);
QNetworkRequest tokenRequest(QUrl(options_.accessTokenUrl.toString() + query));
tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM);
tokenRequest.setRawHeader("Accept", MIME_TYPE_JSON);
QMap<QString, QString> parameters;
parameters.insert(OAUTH2_GRANT_TYPE_CODE, access_code);
parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier);
if ( !options_.clientSecret.isEmpty() ) {
parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret);
}
parameters.insert(OAUTH2_REDIRECT_URI, redirectUri_);
parameters.insert(OAUTH2_GRANT_TYPE, AUTHORIZATION_CODE);
QByteArray data = buildRequestBody(parameters);
qDebug() << QString("OAuth2::onVerificationReceived: Exchange access code data:\n%1").arg(QString(data));
QNetworkReply *tokenReply = manager_->post(tokenRequest, data);
timedReplies_.add(tokenReply);
connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection);
connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
} else if (grantFlow_ == GrantFlowImplicit || grantFlow_ == GrantFlowDevice) {
// Check for mandatory tokens
if (response.contains(OAUTH2_ACCESS_TOKEN)) {
qDebug() << "OAuth2::onVerificationReceived: Access token returned for implicit or device flow";
setToken(response.value(OAUTH2_ACCESS_TOKEN));
if (response.contains(OAUTH2_EXPIRES_IN)) {
bool ok = false;
int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok);
if (ok) {
qDebug() << "OAuth2::onVerificationReceived: Token expires in" << expiresIn << "seconds";
setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn));
}
}
if (response.contains(OAUTH2_REFRESH_TOKEN)) {
setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN));
}
setLinked(true);
emit linkingSucceeded();
} else {
qWarning() << "OAuth2::onVerificationReceived: Access token missing from response for implicit or device flow";
emit linkingFailed();
}
updateActivity(Activity::Idle);
} else {
setToken(response.value(OAUTH2_ACCESS_TOKEN));
setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN));
updateActivity(Activity::Idle);
}
}
void OAuth2::onTokenReplyFinished() {
qDebug() << "OAuth2::onTokenReplyFinished";
QNetworkReply *tokenReply = qobject_cast<QNetworkReply *>(sender());
if (!tokenReply)
{
qDebug() << "OAuth2::onTokenReplyFinished: reply is null";
return;
}
if (tokenReply->error() == QNetworkReply::NoError) {
QByteArray replyData = tokenReply->readAll();
// Dump replyData
// SENSITIVE DATA in RelWithDebInfo or Debug builds
//qDebug() << "OAuth2::onTokenReplyFinished: replyData\n";
//qDebug() << QString( replyData );
QVariantMap tokens = parseJsonResponse(replyData);
// Dump tokens
qDebug() << "OAuth2::onTokenReplyFinished: Tokens returned:\n";
foreach (QString key, tokens.keys()) {
// SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first
qDebug() << key << ": "<< tokens.value( key ).toString();
}
// Check for mandatory tokens
if (tokens.contains(OAUTH2_ACCESS_TOKEN)) {
qDebug() << "OAuth2::onTokenReplyFinished: Access token returned";
setToken(tokens.take(OAUTH2_ACCESS_TOKEN).toString());
bool ok = false;
int expiresIn = tokens.take(OAUTH2_EXPIRES_IN).toInt(&ok);
if (ok) {
qDebug() << "OAuth2::onTokenReplyFinished: Token expires in" << expiresIn << "seconds";
setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn));
}
setRefreshToken(tokens.take(OAUTH2_REFRESH_TOKEN).toString());
setExtraTokens(tokens);
timedReplies_.remove(tokenReply);
setLinked(true);
emit linkingSucceeded();
} else {
qWarning() << "OAuth2::onTokenReplyFinished: Access token missing from response";
emit linkingFailed();
}
}
tokenReply->deleteLater();
updateActivity(Activity::Idle);
}
void OAuth2::onTokenReplyError(QNetworkReply::NetworkError error) {
QNetworkReply *tokenReply = qobject_cast<QNetworkReply *>(sender());
if (!tokenReply)
{
qDebug() << "OAuth2::onTokenReplyError: reply is null";
} else {
qWarning() << "OAuth2::onTokenReplyError: " << error << ": " << tokenReply->errorString();
qDebug() << "OAuth2::onTokenReplyError: " << tokenReply->readAll();
timedReplies_.remove(tokenReply);
}
setToken(QString());
setRefreshToken(QString());
emit linkingFailed();
}
QByteArray OAuth2::buildRequestBody(const QMap<QString, QString> &parameters) {
QByteArray body;
bool first = true;
foreach (QString key, parameters.keys()) {
if (first) {
first = false;
} else {
body.append("&");
}
QString value = parameters.value(key);
body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value));
}
return body;
}
QDateTime OAuth2::expires() {
return token_.notAfter;
}
void OAuth2::setExpires(QDateTime v) {
token_.notAfter = v;
}
void OAuth2::startPollServer(const QVariantMap &params, int expiresIn)
{
qDebug() << "OAuth2::startPollServer: device_ and user_code expires in" << expiresIn << "seconds";
QUrl url(options_.accessTokenUrl);
QNetworkRequest authRequest(url);
authRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString();
const QString grantType = grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_;
QList<RequestParameter> parameters;
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
if ( !options_.clientSecret.isEmpty() ) {
parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8()));
}
parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8()));
parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8()));
QByteArray payload = createQueryParameters(parameters);
PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this);
if (params.contains(OAUTH2_INTERVAL)) {
bool ok = false;
int interval = params[OAUTH2_INTERVAL].toInt(&ok);
if (ok)
pollServer->setInterval(interval);
}
connect(pollServer, SIGNAL(verificationReceived(QMap<QString,QString>)), this, SLOT(onVerificationReceived(QMap<QString,QString>)));
connect(pollServer, SIGNAL(serverClosed(bool)), this, SLOT(serverHasClosed(bool)));
setPollServer(pollServer);
pollServer->startPolling();
}
QString OAuth2::refreshToken() {
return token_.refresh_token;
}
void OAuth2::setRefreshToken(const QString &v) {
#ifndef NDEBUG
qDebug() << "OAuth2::setRefreshToken" << v << "...";
#endif
token_.refresh_token = v;
}
bool OAuth2::refresh() {
qDebug() << "OAuth2::refresh: Token: ..." << refreshToken().right(7);
if (refreshToken().isEmpty()) {
qWarning() << "OAuth2::refresh: No refresh token";
onRefreshError(QNetworkReply::AuthenticationRequiredError);
return false;
}
if (options_.accessTokenUrl.isEmpty()) {
qWarning() << "OAuth2::refresh: Refresh token URL not set";
onRefreshError(QNetworkReply::AuthenticationRequiredError);
return false;
}
updateActivity(Activity::Refreshing);
QNetworkRequest refreshRequest(options_.accessTokenUrl);
refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM);
QMap<QString, QString> parameters;
parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier);
if ( !options_.clientSecret.isEmpty() ) {
parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret);
}
parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken());
parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN);
QByteArray data = buildRequestBody(parameters);
QNetworkReply *refreshReply = manager_->post(refreshRequest, data);
timedReplies_.add(refreshReply);
connect(refreshReply, SIGNAL(finished()), this, SLOT(onRefreshFinished()), Qt::QueuedConnection);
connect(refreshReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRefreshError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
return true;
}
void OAuth2::onRefreshFinished() {
QNetworkReply *refreshReply = qobject_cast<QNetworkReply *>(sender());
if (refreshReply->error() == QNetworkReply::NoError) {
QByteArray reply = refreshReply->readAll();
QVariantMap tokens = parseJsonResponse(reply);
setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString());
setExpires(QDateTime::currentDateTimeUtc().addSecs(tokens.value(OAUTH2_EXPIRES_IN).toInt()));
QString refreshToken = tokens.value(OAUTH2_REFRESH_TOKEN).toString();
if(!refreshToken.isEmpty()) {
setRefreshToken(refreshToken);
}
else {
qDebug() << "No new refresh token. Keep the old one.";
}
timedReplies_.remove(refreshReply);
setLinked(true);
emit linkingSucceeded();
emit refreshFinished(QNetworkReply::NoError);
qDebug() << "New token expires in" << expires() << "seconds";
} else {
emit linkingFailed();
qDebug() << "OAuth2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString();
}
refreshReply->deleteLater();
updateActivity(Activity::Idle);
}
void OAuth2::onRefreshError(QNetworkReply::NetworkError error) {
QNetworkReply *refreshReply = qobject_cast<QNetworkReply *>(sender());
qWarning() << "OAuth2::onRefreshError: " << error;
unlink();
timedReplies_.remove(refreshReply);
emit refreshFinished(error);
}
void OAuth2::onDeviceAuthReplyFinished()
{
qDebug() << "OAuth2::onDeviceAuthReplyFinished";
QNetworkReply *tokenReply = qobject_cast<QNetworkReply *>(sender());
if (!tokenReply)
{
qDebug() << "OAuth2::onDeviceAuthReplyFinished: reply is null";
return;
}
if (tokenReply->error() == QNetworkReply::NoError) {
QByteArray replyData = tokenReply->readAll();
// Dump replyData
// SENSITIVE DATA in RelWithDebInfo or Debug builds
//qDebug() << "OAuth2::onDeviceAuthReplyFinished: replyData\n";
//qDebug() << QString( replyData );
QVariantMap params = parseJsonResponse(replyData);
// Dump tokens
qDebug() << "OAuth2::onDeviceAuthReplyFinished: Tokens returned:\n";
foreach (QString key, params.keys()) {
// SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first
qDebug() << key << ": "<< params.value( key ).toString();
}
// Check for mandatory parameters
if (hasMandatoryDeviceAuthParams(params)) {
qDebug() << "OAuth2::onDeviceAuthReplyFinished: Device auth request response";
const QString userCode = params.take(OAUTH2_USER_CODE).toString();
QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl();
if (uri.isEmpty())
uri = params.take(OAUTH2_VERIFICATION_URL).toUrl();
if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE))
emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl());
bool ok = false;
int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok);
if (!ok) {
qWarning() << "OAuth2::startPollServer: No expired_in parameter";
emit linkingFailed();
return;
}
emit showVerificationUriAndCode(uri, userCode, expiresIn);
startPollServer(params, expiresIn);
} else {
qWarning() << "OAuth2::onDeviceAuthReplyFinished: Mandatory parameters missing from response";
emit linkingFailed();
updateActivity(Activity::Idle);
}
}
tokenReply->deleteLater();
}
void OAuth2::serverHasClosed(bool paramsfound)
{
if ( !paramsfound ) {
// server has probably timed out after receiving first response
emit linkingFailed();
}
// poll server is not re-used for later auth requests
setPollServer(NULL);
}
QString OAuth2::apiKey() {
return apiKey_;
}
void OAuth2::setApiKey(const QString &value) {
apiKey_ = value;
}
bool OAuth2::ignoreSslErrors() {
return timedReplies_.ignoreSslErrors();
}
void OAuth2::setIgnoreSslErrors(bool ignoreSslErrors) {
timedReplies_.setIgnoreSslErrors(ignoreSslErrors);
}
}

View File

@ -7,25 +7,28 @@ namespace Katabasis {
Reply::Reply(QNetworkReply *r, int timeOut, QObject *parent): QTimer(parent), reply(r) {
setSingleShot(true);
connect(this, SIGNAL(error(QNetworkReply::NetworkError)), reply, SIGNAL(error(QNetworkReply::NetworkError)), Qt::QueuedConnection);
connect(this, SIGNAL(timeout()), this, SLOT(onTimeOut()), Qt::QueuedConnection);
connect(this, &Reply::timeout, this, &Reply::onTimeOut, Qt::QueuedConnection);
start(timeOut);
}
void Reply::onTimeOut() {
emit error(QNetworkReply::TimeoutError);
timedOut = true;
reply->abort();
}
// ----------------------------
ReplyList::~ReplyList() {
foreach (Reply *timedReply, replies_) {
delete timedReply;
}
}
void ReplyList::add(QNetworkReply *reply) {
if (reply && ignoreSslErrors())
reply->ignoreSslErrors();
add(new Reply(reply));
void ReplyList::add(QNetworkReply *reply, int timeOut) {
if (reply && ignoreSslErrors()) {
reply->ignoreSslErrors();
}
add(new Reply(reply, timeOut));
}
void ReplyList::add(Reply *reply) {

View File

@ -1,182 +0,0 @@
#include <QTcpServer>
#include <QTcpSocket>
#include <QByteArray>
#include <QString>
#include <QMap>
#include <QPair>
#include <QTimer>
#include <QStringList>
#include <QUrl>
#include <QDebug>
#include <QUrlQuery>
#include "katabasis/Globals.h"
#include "katabasis/ReplyServer.h"
namespace Katabasis {
ReplyServer::ReplyServer(QObject *parent): QTcpServer(parent),
timeout_(15), maxtries_(3), tries_(0) {
qDebug() << "O2ReplyServer: Starting";
connect(this, SIGNAL(newConnection()), this, SLOT(onIncomingConnection()));
replyContent_ = "<HTML></HTML>";
}
void ReplyServer::onIncomingConnection() {
qDebug() << "O2ReplyServer::onIncomingConnection: Receiving...";
QTcpSocket *socket = nextPendingConnection();
connect(socket, SIGNAL(readyRead()), this, SLOT(onBytesReady()), Qt::UniqueConnection);
connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater()));
// Wait for a bit *after* first response, then close server if no useable data has arrived
// Helps with implicit flow, where a URL fragment may need processed by local user-agent and
// sent as secondary query string callback, or additional requests make it through first,
// like for favicons, etc., before such secondary callbacks are fired
QTimer *timer = new QTimer(socket);
timer->setObjectName("timeoutTimer");
connect(timer, SIGNAL(timeout()), this, SLOT(closeServer()));
timer->setSingleShot(true);
timer->setInterval(timeout() * 1000);
connect(socket, SIGNAL(readyRead()), timer, SLOT(start()));
}
void ReplyServer::onBytesReady() {
if (!isListening()) {
// server has been closed, stop processing queued connections
return;
}
qDebug() << "O2ReplyServer::onBytesReady: Processing request";
// NOTE: on first call, the timeout timer is started
QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
if (!socket) {
qWarning() << "O2ReplyServer::onBytesReady: No socket available";
return;
}
QByteArray reply;
reply.append("HTTP/1.0 200 OK \r\n");
reply.append("Content-Type: text/html; charset=\"utf-8\"\r\n");
reply.append(QString("Content-Length: %1\r\n\r\n").arg(replyContent_.size()).toLatin1());
reply.append(replyContent_);
socket->write(reply);
qDebug() << "O2ReplyServer::onBytesReady: Sent reply";
QByteArray data = socket->readAll();
QMap<QString, QString> queryParams = parseQueryParams(&data);
if (queryParams.isEmpty()) {
if (tries_ < maxtries_ ) {
qDebug() << "O2ReplyServer::onBytesReady: No query params found, waiting for more callbacks";
++tries_;
return;
} else {
tries_ = 0;
qWarning() << "O2ReplyServer::onBytesReady: No query params found, maximum callbacks received";
closeServer(socket, false);
return;
}
}
if (!uniqueState_.isEmpty() && !queryParams.contains(QString(OAUTH2_STATE))) {
qDebug() << "O2ReplyServer::onBytesReady: Malicious or service request";
closeServer(socket, true);
return; // Malicious or service (e.g. favicon.ico) request
}
qDebug() << "O2ReplyServer::onBytesReady: Query params found, closing server";
closeServer(socket, true);
emit verificationReceived(queryParams);
}
QMap<QString, QString> ReplyServer::parseQueryParams(QByteArray *data) {
qDebug() << "O2ReplyServer::parseQueryParams";
//qDebug() << QString("O2ReplyServer::parseQueryParams data:\n%1").arg(QString(*data));
QString splitGetLine = QString(*data).split("\r\n").first();
splitGetLine.remove("GET ");
splitGetLine.remove("HTTP/1.1");
splitGetLine.remove("\r\n");
splitGetLine.prepend("http://localhost");
QUrl getTokenUrl(splitGetLine);
QList< QPair<QString, QString> > tokens;
QUrlQuery query(getTokenUrl);
tokens = query.queryItems();
QMap<QString, QString> queryParams;
QPair<QString, QString> tokenPair;
foreach (tokenPair, tokens) {
// FIXME: We are decoding key and value again. This helps with Google OAuth, but is it mandated by the standard?
QString key = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.first.trimmed().toLatin1()));
QString value = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.second.trimmed().toLatin1()));
queryParams.insert(key, value);
}
return queryParams;
}
void ReplyServer::closeServer(QTcpSocket *socket, bool hasparameters)
{
if (!isListening()) {
return;
}
qDebug() << "O2ReplyServer::closeServer: Initiating";
int port = serverPort();
if (!socket && sender()) {
QTimer *timer = qobject_cast<QTimer*>(sender());
if (timer) {
qWarning() << "O2ReplyServer::closeServer: Closing due to timeout";
timer->stop();
socket = qobject_cast<QTcpSocket *>(timer->parent());
timer->deleteLater();
}
}
if (socket) {
QTimer *timer = socket->findChild<QTimer*>("timeoutTimer");
if (timer) {
qDebug() << "O2ReplyServer::closeServer: Stopping socket's timeout timer";
timer->stop();
}
socket->disconnectFromHost();
}
close();
qDebug() << "O2ReplyServer::closeServer: Closed, no longer listening on port" << port;
emit serverClosed(hasparameters);
}
QByteArray ReplyServer::replyContent() {
return replyContent_;
}
void ReplyServer::setReplyContent(const QByteArray &value) {
replyContent_ = value;
}
int ReplyServer::timeout()
{
return timeout_;
}
void ReplyServer::setTimeout(int timeout)
{
timeout_ = timeout;
}
int ReplyServer::callbackTries()
{
return maxtries_;
}
void ReplyServer::setCallbackTries(int maxtries)
{
maxtries_ = maxtries;
}
QString ReplyServer::uniqueState()
{
return uniqueState_;
}
void ReplyServer::setUniqueState(const QString &state)
{
uniqueState_ = state;
}
}