mirror of
https://github.com/nextcloud/desktop.git
synced 2024-12-22 21:50:30 +03:00
dbde585049
The previous commit 50cd6af394
- Build a webflowcredentials
changed:
src/gui/wizard/flow2authcredspage.cpp in line 135 to use WebFlowCredentials
instead of HttpCredentials.
But the WebFlowCredentials class didn't include code to store and load SSL client
certificates and keys from the keychain.
This commit migrates the useful stuff from the old HttpCredentials class
into WebFlowCredentials.
Successfully tested on Windows. Please test on other systems and verify it's safe! :)
Signed-off-by: Michael Schuster <michael@schuster.ms>
479 lines
15 KiB
C++
479 lines
15 KiB
C++
#include "webflowcredentials.h"
|
|
|
|
#include "creds/httpcredentials.h"
|
|
|
|
#include <QAuthenticator>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkReply>
|
|
#include <QPointer>
|
|
#include <QTimer>
|
|
#include <keychain.h>
|
|
#include <QDialog>
|
|
#include <QVBoxLayout>
|
|
#include <QLabel>
|
|
|
|
#include "accessmanager.h"
|
|
#include "account.h"
|
|
#include "configfile.h"
|
|
#include "theme.h"
|
|
#include "wizard/webview.h"
|
|
#include "webflowcredentialsdialog.h"
|
|
|
|
using namespace QKeychain;
|
|
|
|
namespace OCC {
|
|
|
|
Q_LOGGING_CATEGORY(lcWebFlowCredentials, "sync.credentials.webflow", QtInfoMsg)
|
|
|
|
namespace {
|
|
const char userC[] = "user";
|
|
const char clientCertificatePEMC[] = "_clientCertificatePEM";
|
|
const char clientKeyPEMC[] = "_clientKeyPEM";
|
|
} // ns
|
|
|
|
class WebFlowCredentialsAccessManager : public AccessManager
|
|
{
|
|
public:
|
|
WebFlowCredentialsAccessManager(const WebFlowCredentials *cred, QObject *parent = nullptr)
|
|
: AccessManager(parent)
|
|
, _cred(cred)
|
|
{
|
|
}
|
|
|
|
protected:
|
|
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override
|
|
{
|
|
QNetworkRequest req(request);
|
|
if (!req.attribute(WebFlowCredentials::DontAddCredentialsAttribute).toBool()) {
|
|
if (_cred && !_cred->password().isEmpty()) {
|
|
QByteArray credHash = QByteArray(_cred->user().toUtf8() + ":" + _cred->password().toUtf8()).toBase64();
|
|
req.setRawHeader("Authorization", "Basic " + credHash);
|
|
}
|
|
}
|
|
|
|
if (_cred && !_cred->_clientSslKey.isNull() && !_cred->_clientSslCertificate.isNull()) {
|
|
// SSL configuration
|
|
QSslConfiguration sslConfiguration = req.sslConfiguration();
|
|
sslConfiguration.setLocalCertificate(_cred->_clientSslCertificate);
|
|
sslConfiguration.setPrivateKey(_cred->_clientSslKey);
|
|
req.setSslConfiguration(sslConfiguration);
|
|
}
|
|
|
|
return AccessManager::createRequest(op, req, outgoingData);
|
|
}
|
|
|
|
private:
|
|
// The credentials object dies along with the account, while the QNAM might
|
|
// outlive both.
|
|
QPointer<const WebFlowCredentials> _cred;
|
|
};
|
|
|
|
static void addSettingsToJob(Account *account, QKeychain::Job *job)
|
|
{
|
|
Q_UNUSED(account);
|
|
auto settings = ConfigFile::settingsWithGroup(Theme::instance()->appName());
|
|
settings->setParent(job); // make the job parent to make setting deleted properly
|
|
job->setSettings(settings.release());
|
|
}
|
|
|
|
WebFlowCredentials::WebFlowCredentials()
|
|
: _ready(false)
|
|
, _credentialsValid(false)
|
|
, _keychainMigration(false)
|
|
, _retryOnKeyChainError(false)
|
|
{
|
|
|
|
}
|
|
|
|
WebFlowCredentials::WebFlowCredentials(const QString &user, const QString &password, const QSslCertificate &certificate, const QSslKey &key)
|
|
: _user(user)
|
|
, _password(password)
|
|
, _clientSslKey(key)
|
|
, _clientSslCertificate(certificate)
|
|
, _ready(true)
|
|
, _credentialsValid(true)
|
|
, _keychainMigration(false)
|
|
, _retryOnKeyChainError(false)
|
|
{
|
|
|
|
}
|
|
|
|
QString WebFlowCredentials::authType() const {
|
|
return QString::fromLatin1("webflow");
|
|
}
|
|
|
|
QString WebFlowCredentials::user() const {
|
|
return _user;
|
|
}
|
|
|
|
QString WebFlowCredentials::password() const {
|
|
return _password;
|
|
}
|
|
|
|
QNetworkAccessManager *WebFlowCredentials::createQNAM() const {
|
|
qCInfo(lcWebFlowCredentials()) << "Get QNAM";
|
|
AccessManager *qnam = new WebFlowCredentialsAccessManager(this);
|
|
|
|
connect(qnam, &AccessManager::authenticationRequired, this, &WebFlowCredentials::slotAuthentication);
|
|
connect(qnam, &AccessManager::finished, this, &WebFlowCredentials::slotFinished);
|
|
|
|
return qnam;
|
|
}
|
|
|
|
bool WebFlowCredentials::ready() const {
|
|
return _ready;
|
|
}
|
|
|
|
void WebFlowCredentials::fetchFromKeychain() {
|
|
_wasFetched = true;
|
|
|
|
// Make sure we get the user from the config file
|
|
fetchUser();
|
|
|
|
if (ready()) {
|
|
emit fetched();
|
|
} else {
|
|
qCInfo(lcWebFlowCredentials()) << "Fetch from keychain!";
|
|
fetchFromKeychainHelper();
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::askFromUser() {
|
|
// LoginFlowV2 > WebViewFlow > OAuth > Shib > Basic
|
|
bool useFlow2 = (_account->serverVersionInt() >= Account::makeServerVersion(16, 0, 0));
|
|
|
|
_askDialog = new WebFlowCredentialsDialog(_account, useFlow2);
|
|
|
|
if (!useFlow2) {
|
|
QUrl url = _account->url();
|
|
QString path = url.path() + "/index.php/login/flow";
|
|
url.setPath(path);
|
|
_askDialog->setUrl(url);
|
|
}
|
|
|
|
QString msg = tr("You have been logged out of %1 as user %2. Please login again")
|
|
.arg(_account->displayName(), _user);
|
|
_askDialog->setInfo(msg);
|
|
|
|
_askDialog->show();
|
|
|
|
connect(_askDialog, &WebFlowCredentialsDialog::urlCatched, this, &WebFlowCredentials::slotAskFromUserCredentialsProvided);
|
|
|
|
qCDebug(lcWebFlowCredentials()) << "User needs to reauth!";
|
|
}
|
|
|
|
void WebFlowCredentials::slotAskFromUserCredentialsProvided(const QString &user, const QString &pass, const QString &host) {
|
|
Q_UNUSED(host);
|
|
|
|
if (_user != user) {
|
|
qCInfo(lcWebFlowCredentials()) << "Authed with the wrong user!";
|
|
|
|
QString msg = tr("Please login with the user: %1")
|
|
.arg(_user);
|
|
_askDialog->setError(msg);
|
|
|
|
if (!_askDialog->isUsingFlow2()) {
|
|
QUrl url = _account->url();
|
|
QString path = url.path() + "/index.php/login/flow";
|
|
url.setPath(path);
|
|
_askDialog->setUrl(url);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
qCInfo(lcWebFlowCredentials()) << "Obtained a new password";
|
|
|
|
_password = pass;
|
|
_ready = true;
|
|
_credentialsValid = true;
|
|
persist();
|
|
emit asked();
|
|
|
|
_askDialog->close();
|
|
delete _askDialog;
|
|
_askDialog = nullptr;
|
|
}
|
|
|
|
|
|
bool WebFlowCredentials::stillValid(QNetworkReply *reply) {
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
qCWarning(lcWebFlowCredentials()) << reply->error();
|
|
qCWarning(lcWebFlowCredentials()) << reply->errorString();
|
|
}
|
|
return (reply->error() != QNetworkReply::AuthenticationRequiredError);
|
|
}
|
|
|
|
void WebFlowCredentials::persist() {
|
|
if (_user.isEmpty()) {
|
|
// We don't even have a user nothing to see here move along
|
|
return;
|
|
}
|
|
|
|
_account->setCredentialSetting(userC, _user);
|
|
_account->wantsAccountSaved(_account);
|
|
|
|
// write cert if there is one
|
|
if (!_clientSslCertificate.isNull()) {
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientCertPEMJobDone);
|
|
job->setKey(keychainKey(_account->url().toString(), _user + clientCertificatePEMC, _account->id()));
|
|
job->setBinaryData(_clientSslCertificate.toPem());
|
|
job->start();
|
|
} else {
|
|
// no cert, just write credentials
|
|
slotWriteClientCertPEMJobDone();
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::slotWriteClientCertPEMJobDone()
|
|
{
|
|
// write ssl key if there is one
|
|
if (!_clientSslKey.isNull()) {
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientKeyPEMJobDone);
|
|
job->setKey(keychainKey(_account->url().toString(), _user + clientKeyPEMC, _account->id()));
|
|
job->setBinaryData(_clientSslKey.toPem());
|
|
job->start();
|
|
} else {
|
|
// no key, just write credentials
|
|
slotWriteClientKeyPEMJobDone();
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::slotWriteClientKeyPEMJobDone()
|
|
{
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteJobDone);
|
|
job->setKey(keychainKey(_account->url().toString(), _user, _account->id()));
|
|
job->setTextData(_password);
|
|
job->start();
|
|
}
|
|
|
|
void WebFlowCredentials::slotWriteJobDone(QKeychain::Job *job)
|
|
{
|
|
delete job->settings();
|
|
switch (job->error()) {
|
|
case NoError:
|
|
break;
|
|
default:
|
|
qCWarning(lcWebFlowCredentials) << "Error while writing password" << job->errorString();
|
|
}
|
|
WritePasswordJob *wjob = qobject_cast<WritePasswordJob *>(job);
|
|
wjob->deleteLater();
|
|
}
|
|
|
|
void WebFlowCredentials::invalidateToken() {
|
|
// clear the session cookie.
|
|
_account->clearCookieJar();
|
|
|
|
// let QNAM forget about the password
|
|
// This needs to be done later in the event loop because we might be called (directly or
|
|
// indirectly) from QNetworkAccessManagerPrivate::authenticationRequired, which itself
|
|
// is a called from a BlockingQueuedConnection from the Qt HTTP thread. And clearing the
|
|
// cache needs to synchronize again with the HTTP thread.
|
|
QTimer::singleShot(0, _account, &Account::clearQNAMCache);
|
|
}
|
|
|
|
void WebFlowCredentials::forgetSensitiveData(){
|
|
_password = QString();
|
|
_ready = false;
|
|
|
|
fetchUser();
|
|
|
|
const QString kck = keychainKey(_account->url().toString(), _user, _account->id());
|
|
if (kck.isEmpty()) {
|
|
qCDebug(lcWebFlowCredentials()) << "InvalidateToken: User is empty, bailing out!";
|
|
return;
|
|
}
|
|
|
|
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
job->start();
|
|
|
|
invalidateToken();
|
|
}
|
|
|
|
void WebFlowCredentials::setAccount(Account *account) {
|
|
AbstractCredentials::setAccount(account);
|
|
if (_user.isEmpty()) {
|
|
fetchUser();
|
|
}
|
|
}
|
|
|
|
QString WebFlowCredentials::fetchUser() {
|
|
_user = _account->credentialSetting(userC).toString();
|
|
return _user;
|
|
}
|
|
|
|
void WebFlowCredentials::slotAuthentication(QNetworkReply *reply, QAuthenticator *authenticator) {
|
|
Q_UNUSED(reply);
|
|
|
|
if (!_ready) {
|
|
return;
|
|
}
|
|
|
|
if (_credentialsValid == false) {
|
|
return;
|
|
}
|
|
|
|
qCDebug(lcWebFlowCredentials()) << "Requires authentication";
|
|
|
|
authenticator->setUser(_user);
|
|
authenticator->setPassword(_password);
|
|
_credentialsValid = false;
|
|
}
|
|
|
|
void WebFlowCredentials::slotFinished(QNetworkReply *reply) {
|
|
qCInfo(lcWebFlowCredentials()) << "request finished";
|
|
|
|
if (reply->error() == QNetworkReply::NoError) {
|
|
_credentialsValid = true;
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::fetchFromKeychainHelper() {
|
|
// Read client cert from keychain
|
|
const QString kck = keychainKey(
|
|
_account->url().toString(),
|
|
_user + clientCertificatePEMC,
|
|
_keychainMigration ? QString() : _account->id());
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone);
|
|
job->start();
|
|
}
|
|
|
|
void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJob)
|
|
{
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
Q_ASSERT(!incomingJob->insecureFallback()); // If insecureFallback is set, the next test would be pointless
|
|
if (_retryOnKeyChainError && (incomingJob->error() == QKeychain::NoBackendAvailable
|
|
|| incomingJob->error() == QKeychain::OtherError)) {
|
|
// Could be that the backend was not yet available. Wait some extra seconds.
|
|
// (Issues #4274 and #6522)
|
|
// (For kwallet, the error is OtherError instead of NoBackendAvailable, maybe a bug in QtKeychain)
|
|
qCInfo(lcWebFlowCredentials) << "Backend unavailable (yet?) Retrying in a few seconds." << incomingJob->errorString();
|
|
QTimer::singleShot(10000, this, &WebFlowCredentials::fetchFromKeychainHelper);
|
|
_retryOnKeyChainError = false;
|
|
return;
|
|
}
|
|
_retryOnKeyChainError = false;
|
|
#endif
|
|
|
|
// Store PEM in memory
|
|
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
|
|
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
|
|
QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem);
|
|
if (sslCertificateList.length() >= 1) {
|
|
_clientSslCertificate = sslCertificateList.at(0);
|
|
}
|
|
}
|
|
|
|
// Load key too
|
|
const QString kck = keychainKey(
|
|
_account->url().toString(),
|
|
_user + clientKeyPEMC,
|
|
_keychainMigration ? QString() : _account->id());
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone);
|
|
job->start();
|
|
}
|
|
|
|
void WebFlowCredentials::slotReadClientKeyPEMJobDone(QKeychain::Job *incomingJob)
|
|
{
|
|
// Store key in memory
|
|
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
|
|
|
|
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
|
|
QByteArray clientKeyPEM = readJob->binaryData();
|
|
// FIXME Unfortunately Qt has a bug and we can't just use QSsl::Opaque to let it
|
|
// load whatever we have. So we try until it works.
|
|
_clientSslKey = QSslKey(clientKeyPEM, QSsl::Rsa);
|
|
if (_clientSslKey.isNull()) {
|
|
_clientSslKey = QSslKey(clientKeyPEM, QSsl::Dsa);
|
|
}
|
|
if (_clientSslKey.isNull()) {
|
|
_clientSslKey = QSslKey(clientKeyPEM, QSsl::Ec);
|
|
}
|
|
if (_clientSslKey.isNull()) {
|
|
qCWarning(lcWebFlowCredentials) << "Could not load SSL key into Qt!";
|
|
}
|
|
}
|
|
|
|
// Now fetch the actual server password
|
|
const QString kck = keychainKey(
|
|
_account->url().toString(),
|
|
_user,
|
|
_keychainMigration ? QString() : _account->id());
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadPasswordJobDone);
|
|
job->start();
|
|
}
|
|
|
|
void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
|
|
QKeychain::ReadPasswordJob *job = static_cast<ReadPasswordJob *>(incomingJob);
|
|
QKeychain::Error error = job->error();
|
|
|
|
// If we could not find the entry try the old entries
|
|
if (!_keychainMigration && error == QKeychain::EntryNotFound) {
|
|
_keychainMigration = true;
|
|
fetchFromKeychainHelper();
|
|
return;
|
|
}
|
|
|
|
if (_user.isEmpty()) {
|
|
qCWarning(lcWebFlowCredentials) << "Strange: User is empty!";
|
|
}
|
|
|
|
if (error == QKeychain::NoError) {
|
|
_password = job->textData();
|
|
_ready = true;
|
|
_credentialsValid = true;
|
|
} else {
|
|
_ready = false;
|
|
}
|
|
emit fetched();
|
|
|
|
// If keychain data was read from legacy location, wipe these entries and store new ones
|
|
if (_keychainMigration && _ready) {
|
|
_keychainMigration = false;
|
|
persist();
|
|
deleteOldKeychainEntries();
|
|
qCInfo(lcWebFlowCredentials) << "Migrated old keychain entries";
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::deleteOldKeychainEntries() {
|
|
auto startDeleteJob = [this](QString user) {
|
|
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(true);
|
|
job->setKey(keychainKey(_account->url().toString(), user, QString()));
|
|
job->start();
|
|
};
|
|
|
|
startDeleteJob(_user);
|
|
startDeleteJob(_user + clientKeyPEMC);
|
|
startDeleteJob(_user + clientCertificatePEMC);
|
|
}
|
|
|
|
}
|