diff --git a/providers/ghprovider/ghdialog.h b/providers/ghprovider/ghdialog.h --- a/providers/ghprovider/ghdialog.h +++ b/providers/ghprovider/ghdialog.h @@ -74,6 +74,11 @@ */ void authorizeResponse(const QByteArray &id, const QByteArray &token, const QString &tokenName); + /** + * Handle a two factor response from GitHub during the authorization process. + */ + void twoFactorResponse(); + /// Sync the user's groups. void syncUser(); diff --git a/providers/ghprovider/ghdialog.cpp b/providers/ghprovider/ghdialog.cpp --- a/providers/ghprovider/ghdialog.cpp +++ b/providers/ghprovider/ghdialog.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -114,6 +115,8 @@ Resource *rs = m_account->resource(); rs->authenticate(dlg->username(), dlg->password()); + connect(rs, &Resource::twoFactorRequested, + this, &Dialog::twoFactorResponse); connect(rs, &Resource::authenticated, this, &Dialog::authorizeResponse); } @@ -148,6 +151,16 @@ syncUser(); } +void Dialog::twoFactorResponse() +{ + auto code = QInputDialog::getText(this, i18n("Authentication Code"), i18n("Authentication Code")); + Resource* rs = m_account->resource(); + disconnect(rs, &Resource::twoFactorRequested, + this, &Dialog::twoFactorResponse); + + rs->twoFactorAuthenticate(code); +} + void Dialog::syncUser() { Resource *rs = m_account->resource(); diff --git a/providers/ghprovider/ghresource.h b/providers/ghprovider/ghresource.h --- a/providers/ghprovider/ghresource.h +++ b/providers/ghprovider/ghresource.h @@ -83,6 +83,14 @@ void authenticate(const QString &name, const QString &password); /** + * Authenticate the current user with the two factor authentication code. + * Must be called after a call to authenticate. + * + * @param code The two factor authentication code. + */ + void twoFactorAuthenticate(const QString &code); + + /** * Revoke an access to the Github API (a.k.a. log out the current user). * * @param id The id of the currently valid authorization. @@ -132,6 +140,11 @@ void authenticated(const QByteArray &id, const QByteArray &token, const QString &tokenName); /** + * This signal is sent if two factor authentication is requested by GitHub. + */ + void twoFactorRequested(); + + /** * This signal is emitted when the model containing repos has * been updated. */ @@ -173,6 +186,7 @@ private: ProviderModel *m_model; QByteArray m_temp, m_orgTemp; + QString m_tfHttpHeader; }; } // End of namespace gh diff --git a/providers/ghprovider/ghresource.cpp b/providers/ghprovider/ghresource.cpp --- a/providers/ghprovider/ghresource.cpp +++ b/providers/ghprovider/ghresource.cpp @@ -36,6 +36,25 @@ /// Base url for the Github API v3. const static QUrl baseUrl(QStringLiteral("https://api.github.com")); +KIO::StoredTransferJob* createHttpAuthJob(const QString &httpHeader) +{ + QUrl url = baseUrl; + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(url.path() + QLatin1String("/authorizations")); + + // generate a unique token, see bug 372144 + const QString tokenName = "KDevelop Github Provider : " + + QHostInfo::localHostName() + " - " + + QDateTime::currentDateTimeUtc().toString(); + const QByteArray data = QByteArrayLiteral("{ \"scopes\": [\"repo\"], \"note\": \"") + tokenName.toUtf8() + QByteArrayLiteral("\" }"); + + KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo); + job->setProperty("requestedTokenName", tokenName); + job->addMetaData(QStringLiteral("customHTTPHeader"), httpHeader); + + return job; +} + Resource::Resource(QObject *parent, ProviderModel *model) : QObject(parent), m_model(model) { @@ -58,20 +77,20 @@ void Resource::authenticate(const QString &name, const QString &password) { - QUrl url = baseUrl; - url = url.adjusted(QUrl::StripTrailingSlash); - url.setPath(url.path() + '/' + "/authorizations"); - - // generate a unique token, see bug 372144 - const QString tokenName = "KDevelop Github Provider : " - + QHostInfo::localHostName() + " - " - + QDateTime::currentDateTimeUtc().toString(); - const QByteArray data = "{ \"scopes\": [\"repo\"], \"note\": \"" + tokenName.toUtf8() + "\" }"; + m_tfHttpHeader.clear(); + auto job = createHttpAuthJob(QLatin1String("Authorization: Basic ") + QString::fromUtf8((name.toUtf8() + ':' + password.toUtf8()).toBase64())); + job->addMetaData("PropagateHttpHeader","true"); + connect(job, &KIO::StoredTransferJob::result, + this, &Resource::slotAuthenticate); + job->start(); +} - KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo); - job->setProperty("requestedTokenName", tokenName); - job->addMetaData(QStringLiteral("customHTTPHeader"), "Authorization: Basic " + QString (name + ':' + password).toUtf8().toBase64()); - connect(job, &KIO::StoredTransferJob::result, this, &Resource::slotAuthenticate); +void Resource::twoFactorAuthenticate(const QString &code) +{ + auto job = createHttpAuthJob(m_tfHttpHeader + QLatin1String("\nX-GitHub-OTP: ") + code); + m_tfHttpHeader.clear(); + connect(job, &KIO::StoredTransferJob::result, + this, &Resource::slotAuthenticate); job->start(); } @@ -148,6 +167,16 @@ return; } + const auto metaData = qobject_cast(job)->metaData(); + if (metaData[QStringLiteral("responsecode")] == QStringLiteral("401")) { + const auto& header = metaData[QStringLiteral("HTTP-Headers")]; + if (header.contains(QStringLiteral("x-github-otp: required;"), Qt::CaseSensitive)) { + m_tfHttpHeader = qobject_cast(job)->outgoingMetaData()[QStringLiteral("customHTTPHeader")]; + emit twoFactorRequested(); + return; + } + } + QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(qobject_cast(job)->data(), &error);