nextcloud-desktop/src/creds/shibbolethcredentials.cpp
Daniel Molkentin 1544606bf0 Always handle 401 requests, except for network jobs
It is not enough to only implement it for the QNAM returned
by the ShibbolethCredentials, because we sometimes need it
when we have no valid credentials set (and are using dummy
credentials in the course).  The main use case is the
Webview opened by Shibboleth for FBA.

But as a side-effect, we can use it to handle auth requests
from the updater and other places.
2014-08-12 19:25:59 +02:00

441 lines
14 KiB
C++

/*
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program 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; version 2 of the License.
*
* This program 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.
*/
#include <QMutex>
#include <QSettings>
#include <QNetworkReply>
#include <QMessageBox>
#include <QAuthenticator>
#include <QDebug>
#include "creds/shibbolethcredentials.h"
#include "creds/shibboleth/shibbolethwebview.h"
#include "creds/shibboleth/shibbolethrefresher.h"
#include "creds/shibbolethcredentials.h"
#include "shibboleth/shibbolethuserjob.h"
#include "creds/credentialscommon.h"
#include "mirall/mirallaccessmanager.h"
#include "mirall/account.h"
#include "mirall/theme.h"
#include "mirall/cookiejar.h"
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
#include <qt5keychain/keychain.h>
#else
#include <qtkeychain/keychain.h>
#endif
using namespace QKeychain;
namespace Mirall
{
namespace
{
// Not "user" because it has a special meaning for http
const char userC[] = "shib_user";
const char shibCookieNameC[] = "_shibsession_";
int shibboleth_redirect_callback(CSYNC* csync_ctx,
const char* uri)
{
if (!csync_ctx || !uri) {
return 1;
}
const QString qurl(QString::fromLatin1(uri));
QRegExp shibbolethyWords ("SAML|wayf");
shibbolethyWords.setCaseSensitivity (Qt::CaseInsensitive);
if (!qurl.contains(shibbolethyWords)) {
return 1;
}
QMutex mutex;
QMutexLocker locker(&mutex);
Account *account = AccountManager::instance()->account();
ShibbolethCredentials* creds = qobject_cast<ShibbolethCredentials*>(account->credentials());
if (!creds) {
qDebug() << "Not a Shibboleth creds instance!";
return 1;
}
ShibbolethRefresher refresher(account, creds, csync_ctx);
// blocks
refresher.refresh();
return creds->ready() ? 0 : 1;
}
} // ns
ShibbolethCredentials::ShibbolethCredentials()
: AbstractCredentials(),
_url(),
_ready(false),
_stillValid(false),
_fetchJobInProgress(false),
_browser(0)
{}
ShibbolethCredentials::ShibbolethCredentials(const QNetworkCookie& cookie, Account* account)
: _ready(true),
_stillValid(true),
_fetchJobInProgress(false),
_browser(0),
_shibCookie(cookie)
{
if (account) {
/* The _user has not yet been fetched, so fetch it now */
ShibbolethUserJob *job = new ShibbolethUserJob(account, this);
connect(job, SIGNAL(userFetched(QString)), this, SLOT(slotUserFetched(QString)));
QTimer::singleShot(1234, job, SLOT(start()));
}
}
void ShibbolethCredentials::syncContextPreInit(CSYNC* ctx)
{
csync_set_auth_callback (ctx, handleNeonSSLProblems);
}
QByteArray ShibbolethCredentials::prepareCookieData() const
{
QString cookiesAsString;
// TODO: This should not be a part of this method, but we don't
// have any way to get "session_key" module property from
// csync. Had we have it, then we could just append shibboleth
// cookies to the "session_key" value and set it in csync module.
Account *account = AccountManager::instance()->account();
QList<QNetworkCookie> cookies = accountCookies(account);
foreach(const QNetworkCookie &cookie, cookies) {
cookiesAsString += cookie.toRawForm(QNetworkCookie::NameAndValueOnly) + QLatin1String("; ");
}
return cookiesAsString.toLatin1();
}
void ShibbolethCredentials::syncContextPreStart (CSYNC* ctx)
{
typedef int (*csync_owncloud_redirect_callback_t)(CSYNC* ctx, const char* uri);
csync_owncloud_redirect_callback_t cb = shibboleth_redirect_callback;
csync_set_module_property(ctx, "session_key", prepareCookieData().data());
csync_set_module_property(ctx, "redirect_callback", &cb);
}
bool ShibbolethCredentials::changed(AbstractCredentials* credentials) const
{
ShibbolethCredentials* other(qobject_cast< ShibbolethCredentials* >(credentials));
if (!other) {
return true;
}
if (_shibCookie != other->_shibCookie || _user != other->_user) {
return true;
}
return false;
}
QString ShibbolethCredentials::authType() const
{
return QString::fromLatin1("shibboleth");
}
QString ShibbolethCredentials::user() const
{
return _user;
}
QNetworkAccessManager* ShibbolethCredentials::getQNAM() const
{
QNetworkAccessManager* qnam(new MirallAccessManager);
connect(qnam, SIGNAL(finished(QNetworkReply*)),
this, SLOT(slotReplyFinished(QNetworkReply*)));
return qnam;
}
void ShibbolethCredentials::slotReplyFinished(QNetworkReply* r)
{
if (!_browser.isNull()) {
return;
}
QVariant target = r->attribute(QNetworkRequest::RedirectionTargetAttribute);
if (target.isValid()) {
_stillValid = false;
qWarning() << Q_FUNC_INFO << "detected redirect, will open Login Window"; // will be done in NetworkJob's finished signal
} else {
//_stillValid = true; // gets set when reading from keychain or getting it from browser
}
}
bool ShibbolethCredentials::ready() const
{
return _ready;
}
void ShibbolethCredentials::fetch(Account *account)
{
if(_fetchJobInProgress) {
return;
}
if (_user.isEmpty()) {
_user = account->credentialSetting(QLatin1String(userC)).toString();
}
if (_ready) {
_fetchJobInProgress = false;
Q_EMIT fetched();
} else {
if (account) {
_url = account->url();
}
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job));
job->setInsecureFallback(false);
job->setKey(keychainKey(account->url().toString(), "shibAssertion"));
job->setProperty("account", QVariant::fromValue(account));
connect(job, SIGNAL(finished(QKeychain::Job*)), SLOT(slotReadJobDone(QKeychain::Job*)));
job->start();
_fetchJobInProgress = true;
}
}
bool ShibbolethCredentials::stillValid(QNetworkReply *reply)
{
Q_UNUSED(reply)
return _stillValid;
}
void ShibbolethCredentials::persist(Account* account)
{
storeShibCookie(_shibCookie, account);
if (!_user.isEmpty()) {
account->setCredentialSetting(QLatin1String(userC), _user);
}
}
// only used by Application::slotLogout(). Use invalidateAndFetch for normal usage
void ShibbolethCredentials::invalidateToken(Account *account)
{
CookieJar *jar = static_cast<CookieJar*>(account->networkAccessManager()->cookieJar());
jar->deleteCookie(_shibCookie);
jar->clearSessionCookies();
removeShibCookie(account);
_shibCookie = QNetworkCookie();
// ### access to ctx missing, but might not be required at all
//csync_set_module_property(ctx, "session_key", "");
}
void ShibbolethCredentials::onShibbolethCookieReceived(const QNetworkCookie& shibCookie, Account *account)
{
storeShibCookie(shibCookie, account);
_shibCookie = shibCookie;
addToCookieJar(shibCookie);
// Now fetch the user...
// But we must first do a request to webdav so the session is enabled.
// (because for some reason we wan't access the API without that.. a bug in the server maybe?)
EntityExistsJob* job = new EntityExistsJob(account, account->davPath(), this);
connect(job, SIGNAL(exists(QNetworkReply*)), this, SLOT(slotFetchUser()));
job->setIgnoreCredentialFailure(true);
job->start();
}
void ShibbolethCredentials::slotFetchUser()
{
AbstractNetworkJob* oldjob = qobject_cast<AbstractNetworkJob*>(sender());
Q_ASSERT(oldjob);
ShibbolethUserJob *job = new ShibbolethUserJob(oldjob->account(), this);
connect(job, SIGNAL(userFetched(QString)), this, SLOT(slotUserFetched(QString)));
job->start();
}
void ShibbolethCredentials::slotUserFetched(const QString &user)
{
ShibbolethUserJob *job = qobject_cast<ShibbolethUserJob *>(sender());
Q_ASSERT(job);
if (_user.isEmpty()) {
_user = user;
} else if (user != _user) {
qDebug() << "Wrong user: " << user << "!=" << _user;
QMessageBox::warning(_browser, tr("Login Error"), tr("You must sign in as user %1").arg(_user));
invalidateToken(job->account());
showLoginWindow(job->account());
return;
}
_stillValid = true;
_ready = true;
_fetchJobInProgress = false;
Q_EMIT fetched();
}
void ShibbolethCredentials::slotBrowserRejected()
{
_ready = false;
_fetchJobInProgress = false;
Q_EMIT fetched();
}
void ShibbolethCredentials::invalidateAndFetch(Account* account)
{
_ready = false;
_fetchJobInProgress = true;
// delete the credentials, then in the slot fetch them again (which will trigger browser)
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
job->setProperty("account", QVariant::fromValue(account));
job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job));
connect(job, SIGNAL(finished(QKeychain::Job*)), SLOT(slotInvalidateAndFetchInvalidateDone(QKeychain::Job*)));
job->setKey(keychainKey(account->url().toString(), "shibAssertion"));
job->start();
}
void ShibbolethCredentials::slotInvalidateAndFetchInvalidateDone(QKeychain::Job* job)
{
Account *account = qvariant_cast<Account*>(job->property("account"));
connect (this, SIGNAL(fetched()),
this, SLOT(onFetched()));
_fetchJobInProgress = false;
// small hack to support the ShibbolethRefresher hack
// we already rand fetch() with a valid account object,
// and hence know the url on refresh
fetch(account);
}
void ShibbolethCredentials::onFetched()
{
disconnect (this, SIGNAL(fetched()),
this, SLOT(onFetched()));
Q_EMIT invalidatedAndFetched(prepareCookieData());
}
void ShibbolethCredentials::slotReadJobDone(QKeychain::Job *job)
{
Account *account = qvariant_cast<Account*>(job->property("account"));
if (job->error() == QKeychain::NoError) {
ReadPasswordJob *readJob = static_cast<ReadPasswordJob*>(job);
delete readJob->settings();
QList<QNetworkCookie> cookies = QNetworkCookie::parseCookies(readJob->textData().toUtf8());
if (cookies.count() > 0) {
_shibCookie = cookies.first();
addToCookieJar(_shibCookie);
}
// access
job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job));
_ready = true;
_stillValid = true;
_fetchJobInProgress = false;
Q_EMIT fetched();
} else {
showLoginWindow(account);
}
}
void ShibbolethCredentials::showLoginWindow(Account* account)
{
if (!_browser.isNull()) {
_browser->activateWindow();
_browser->raise();
// FIXME On OS X this does not raise properly
return;
}
CookieJar *jar = static_cast<CookieJar*>(account->networkAccessManager()->cookieJar());
// When opening a new window clear all the session cookie that might keep the user from logging in
// (or the session may already be open in the server, and there will not be redirect asking for the
// real long term cookie we want to store)
jar->clearSessionCookies();
_browser = new ShibbolethWebView(account);
connect(_browser, SIGNAL(shibbolethCookieReceived(QNetworkCookie, Account*)),
this, SLOT(onShibbolethCookieReceived(QNetworkCookie, Account*)), Qt::QueuedConnection);
connect(_browser, SIGNAL(rejected()), this, SLOT(slotBrowserRejected()));
_browser->show();
}
QList<QNetworkCookie> ShibbolethCredentials::accountCookies(Account *account)
{
return account->networkAccessManager()->cookieJar()->cookiesForUrl(account->url());
}
QNetworkCookie ShibbolethCredentials::findShibCookie(Account *account, QList<QNetworkCookie> cookies)
{
if(cookies.isEmpty()) {
cookies = accountCookies(account);
}
Q_FOREACH(QNetworkCookie cookie, cookies) {
if (cookie.name().startsWith(shibCookieNameC)) {
return cookie;
}
}
return QNetworkCookie();
}
QByteArray ShibbolethCredentials::shibCookieName()
{
return QByteArray(shibCookieNameC);
}
void ShibbolethCredentials::storeShibCookie(const QNetworkCookie &cookie, Account *account)
{
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job));
// we don't really care if it works...
//connect(job, SIGNAL(finished(QKeychain::Job*)), SLOT(slotWriteJobDone(QKeychain::Job*)));
job->setKey(keychainKey(account->url().toString(), "shibAssertion"));
job->setTextData(QString::fromUtf8(cookie.toRawForm()));
job->start();
}
void ShibbolethCredentials::removeShibCookie(Account *account)
{
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
job->setSettings(account->settingsWithGroup(Theme::instance()->appName(), job));
job->setKey(keychainKey(account->url().toString(), "shibAssertion"));
job->start();
}
void ShibbolethCredentials::addToCookieJar(const QNetworkCookie &cookie)
{
QList<QNetworkCookie> cookies;
cookies << cookie;
Account *account = AccountManager::instance()->account();
QNetworkCookieJar *jar = account->networkAccessManager()->cookieJar();
jar->blockSignals(true); // otherwise we'd call ourselves
jar->setCookiesFromUrl(cookies, account->url());
jar->blockSignals(false);
}
} // ns Mirall