Commit 8fb5c619 authored by Jan Grulich's avatar Jan Grulich

Add OTP support for openconnect VPN

Summary:
With this patch, One Time Password (OTP) support is added to the openconnect VPN settings and code. It draws the specifications from NetworkManager-openconnect, and presents the same functionality.
The config dialog is modified to include settings for the OTP options
The auth dialog reads and sets the OTP option, updating tokens as needed

Reviewers: jgrulich

Reviewed By: jgrulich

Subscribers: pino, plasma-devel

Tags: #plasma

Differential Revision: https://phabricator.kde.org/D18394
parent 8355649e
......@@ -39,7 +39,7 @@ if (OPENCONNECT_FOUND)
openconnectauthworkerthread.cpp
)
ki18n_wrap_ui(openconnect_SRCS openconnectprop.ui openconnectauth.ui)
ki18n_wrap_ui(openconnect_SRCS openconnectprop.ui openconnectauth.ui openconnecttoken.ui)
add_library(plasmanetworkmanagement_openconnectui ${openconnect_SRCS})
......
......@@ -38,10 +38,13 @@
#define NM_OPENCONNECT_KEY_PRIVKEY "userkey"
#define NM_OPENCONNECT_KEY_MTU "mtu"
#define NM_OPENCONNECT_KEY_PEM_PASSPHRASE_FSID "pem_passphrase_fsid"
#define NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT "prevent_invalid_cert"
#define NM_OPENCONNECT_KEY_PROTOCOL "protocol"
#define NM_OPENCONNECT_KEY_PROXY "proxy"
#define NM_OPENCONNECT_KEY_CSD_ENABLE "enable_csd_trojan"
#define NM_OPENCONNECT_KEY_CSD_WRAPPER "csd_wrapper"
#define NM_OPENCONNECT_KEY_TOKEN_MODE "stoken_source"
#define NM_OPENCONNECT_KEY_TOKEN_SECRET "stoken_string"
#define NM_OPENCONNECT_USER "nm-openconnect"
......
......@@ -57,6 +57,18 @@ extern "C"
#include <fcntl.h>
}
#if !OPENCONNECT_CHECK_VER(2,1)
#define __openconnect_set_token_mode(...) -EOPNOTSUPP
#elif !OPENCONNECT_CHECK_VER(2,2)
#define __openconnect_set_token_mode(vpninfo, mode, secret) openconnect_set_stoken_mode(vpninfo, 1, secret)
#else
#define __openconnect_set_token_mode openconnect_set_token_mode
#endif
#if OPENCONNECT_CHECK_VER(3,4)
static int updateToken(void*, const char*);
#endif
// name/address: IP/domain name of the host (OpenConnect accepts both, so no difference here)
// group: user group on the server
typedef struct {
......@@ -65,6 +77,11 @@ typedef struct {
QString address;
} VPNHost;
typedef struct {
oc_token_mode_t tokenMode;
QByteArray tokenSecret;
} Token;
class OpenconnectAuthWidgetPrivate
{
public:
......@@ -82,6 +99,8 @@ public:
int cancelPipes[2];
QList<QPair<QString, int> > serverLog;
int passwordFormIndex;
QByteArray tokenMode;
Token token;
enum LogLevels {Error = 0, Info, Debug, Trace};
};
......@@ -122,10 +141,15 @@ OpenconnectAuthWidget::OpenconnectAuthWidget(const NetworkManager::VpnSetting::P
connect(d->worker, &OpenconnectAuthWorkerThread::updateLog, this, &OpenconnectAuthWidget::updateLog);
connect(d->worker, QOverload<const QString&>::of(&OpenconnectAuthWorkerThread::writeNewConfig), this, &OpenconnectAuthWidget::writeNewConfig);
connect(d->worker, &OpenconnectAuthWorkerThread::cookieObtained, this, &OpenconnectAuthWidget::workerFinished);
connect(d->worker, &OpenconnectAuthWorkerThread::initTokens, this, &OpenconnectAuthWidget::initTokens);
readConfig();
readSecrets();
#if OPENCONNECT_CHECK_VER(3,4)
openconnect_set_token_callbacks(d->vpninfo, &d->secrets, NULL, &updateToken);
#endif
// This might be set by readSecrets() so don't connect it until now
connect(d->ui.cmbHosts, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &OpenconnectAuthWidget::connectHost);
......@@ -196,6 +220,8 @@ void OpenconnectAuthWidget::readConfig()
const QString protocol = dataMap[NM_OPENCONNECT_KEY_PROTOCOL];
openconnect_set_protocol(d->vpninfo, OC3DUP(protocol == "juniper" ? "nc" : protocol.toUtf8().data()));
}
d->tokenMode = dataMap[NM_OPENCONNECT_KEY_TOKEN_MODE].toUtf8();
}
void OpenconnectAuthWidget::readSecrets()
......@@ -248,6 +274,40 @@ void OpenconnectAuthWidget::readSecrets()
if (d->secrets["save_passwords"] == "yes") {
d->ui.chkStorePasswords->setChecked(true);
}
d->token.tokenMode = OC_TOKEN_MODE_NONE;
d->token.tokenSecret = nullptr;
if (!d->tokenMode.isEmpty()) {
int ret = 0;
QByteArray tokenSecret = d->secrets[NM_OPENCONNECT_KEY_TOKEN_SECRET].toUtf8();
if (d->tokenMode == QStringLiteral("manual") && !tokenSecret.isEmpty()) {
ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_STOKEN, tokenSecret);
} else if (d->tokenMode ==QStringLiteral("stokenrc")) {
ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_STOKEN, NULL);
} else if (d->tokenMode == QStringLiteral("totp") && !tokenSecret.isEmpty()) {
ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_TOTP, tokenSecret);
}
#if OPENCONNECT_CHECK_VER(3,4)
else if (d->tokenMode == QStringLiteral("hotp") && !tokenSecret.isEmpty()) {
ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_HOTP, tokenSecret);
}
#endif
#if OPENCONNECT_CHECK_VER(5,0)
else if (d->tokenMode == "yubioath") {
/* This needs to be done from a thread because it can call back to
ask for the PIN */
d->token.tokenMode = OC_TOKEN_MODE_YUBIOATH;
if (!tokenSecret.isEmpty()) {
d->token.tokenSecret = tokenSecret;
}
}
#endif
if (ret) {
addFormInfo(QLatin1String("dialog-error"), i18n("Failed to initialize software token: %1", ret));
}
}
}
void OpenconnectAuthWidget::acceptDialog()
......@@ -269,6 +329,7 @@ void OpenconnectAuthWidget::acceptDialog()
void OpenconnectAuthWidget::connectHost()
{
Q_D(OpenconnectAuthWidget);
d->userQuit = true;
if (write(d->cancelPipes[1], "x", 1)) {
// not a lot we can do
......@@ -301,6 +362,15 @@ void OpenconnectAuthWidget::connectHost()
d->worker->start();
}
void OpenconnectAuthWidget::initTokens()
{
Q_D(OpenconnectAuthWidget);
if (d->token.tokenMode != OC_TOKEN_MODE_NONE) {
__openconnect_set_token_mode(d->vpninfo, d->token.tokenMode, d->token.tokenSecret);
}
}
QVariantMap OpenconnectAuthWidget::setting() const
{
Q_D(const OpenconnectAuthWidget);
......@@ -346,6 +416,15 @@ QVariantMap OpenconnectAuthWidget::setting() const
return secretData;
}
#if OPENCONNECT_CHECK_VER(3,4)
static int updateToken(void *cbdata, const char *tok)
{
NMStringMap *secrets = static_cast<NMStringMap *>(cbdata);
secrets->insert(QLatin1String(NM_OPENCONNECT_KEY_TOKEN_SECRET), QLatin1String(tok));
return 0;
}
#endif
void OpenconnectAuthWidget::writeNewConfig(const QString & buf)
{
Q_D(OpenconnectAuthWidget);
......@@ -355,6 +434,7 @@ void OpenconnectAuthWidget::writeNewConfig(const QString & buf)
void OpenconnectAuthWidget::updateLog(const QString &message, const int &level)
{
Q_D(OpenconnectAuthWidget);
QPair<QString, int> pair;
pair.first = message;
if (pair.first.endsWith(QLatin1String("\n"))) {
......@@ -401,6 +481,7 @@ void OpenconnectAuthWidget::logLevelChanged(int newLevel)
void OpenconnectAuthWidget::addFormInfo(const QString &iconName, const QString &message)
{
Q_D(OpenconnectAuthWidget);
QHBoxLayout *layout = new QHBoxLayout();
QLabel *icon = new QLabel(this);
QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
......@@ -426,6 +507,7 @@ void OpenconnectAuthWidget::addFormInfo(const QString &iconName, const QString &
void OpenconnectAuthWidget::processAuthForm(struct oc_auth_form *form)
{
Q_D(OpenconnectAuthWidget);
deleteAllFromLayout(d->ui.loginBoxLayout);
if (form->banner) {
addFormInfo(QLatin1String("dialog-information"), form->banner);
......@@ -571,6 +653,10 @@ void OpenconnectAuthWidget::validatePeerCert(const QString &fingerprint,
connect(buttons, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject);
dialog->layout()->addWidget(widget);
dialog->layout()->addWidget(buttons);
const NMStringMap dataMap = d->setting->data();
buttons->button(QDialogButtonBox::Ok)->setEnabled(dataMap[NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT] != "yes");
if(dialog.data()->exec() == QDialog::Accepted) {
*accepted = true;
} else {
......@@ -641,6 +727,7 @@ void OpenconnectAuthWidget::formLoginClicked()
void OpenconnectAuthWidget::workerFinished(const int &ret)
{
Q_D(OpenconnectAuthWidget);
if (ret < 0) {
QString message;
QList<QPair<QString, int> >::const_iterator i;
......
......@@ -62,6 +62,7 @@ private Q_SLOTS:
void workerFinished(const int&);
void viewServerLogToggled(bool);
void connectHost();
void initTokens();
};
#endif // OPENCONNECTAUTH_H
......@@ -116,6 +116,7 @@ OpenconnectAuthWorkerThread::~OpenconnectAuthWorkerThread()
void OpenconnectAuthWorkerThread::run()
{
openconnect_init_ssl();
Q_EMIT initTokens();
int ret = openconnect_obtain_cookie(m_openconnectInfo);
if (*m_userDecidedToQuit) {
return;
......
......@@ -91,7 +91,7 @@ Q_SIGNALS:
void updateLog(const QString &, const int&);
void writeNewConfig(const QString &);
void cookieObtained(const int&);
void initTokens(void);
protected:
void run() override;
......
......@@ -6,14 +6,20 @@
<rect>
<x>0</x>
<y>0</y>
<width>339</width>
<height>364</height>
<width>418</width>
<height>488</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>OpenConnect Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="grp_general">
<property name="title">
......@@ -94,6 +100,9 @@
<property name="text">
<string>VPN Protocol:</string>
</property>
<property name="buddy">
<cstring>cmbProtocol</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
......@@ -125,9 +134,6 @@
<string>Certificate Authentication</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="verticalSpacing">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
......@@ -138,6 +144,13 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="KUrlRequester" name="leUserCert">
<property name="filter">
<string>*.pem *.crt *.key</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
......@@ -148,13 +161,6 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="KUrlRequester" name="leUserCert">
<property name="filter">
<string>*.pem *.crt *.key</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="KUrlRequester" name="leUserPrivateKey">
<property name="filter">
......@@ -169,6 +175,39 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="preventInvalidCert">
<property name="text">
<string>Prevent user from manually accepting invalid certificates</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>278</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="buTokens">
<property name="text">
<string>Token Authentication</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
......@@ -190,10 +229,13 @@
<customwidgets>
<customwidget>
<class>KUrlRequester</class>
<extends>QWidget</extends>
<extends>QFrame</extends>
<header>kurlrequester.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
<slots>
<slot>enableTokenSecret(int)</slot>
</slots>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OpenConnectToken</class>
<widget class="QWidget" name="OpenConnectToken">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>191</height>
</rect>
</property>
<property name="windowTitle">
<string>OpenConnect OTP Tokens</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="gbToken">
<property name="title">
<string>Software Token Authentication</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Token Mode:</string>
</property>
<property name="buddy">
<cstring>cmbTokenMode</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cmbTokenMode">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Token Secret:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="PasswordField" name="leTokenSecret"/>
</item>
<item row="2" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PasswordField</class>
<extends>QLineEdit</extends>
<header>passwordfield.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
This diff is collapsed.
......@@ -31,13 +31,22 @@ class OpenconnectSettingWidgetPrivate;
class OpenconnectSettingWidget : public SettingWidget
{
Q_OBJECT
Q_DECLARE_PRIVATE(OpenconnectSettingWidget)
public:
explicit OpenconnectSettingWidget(const NetworkManager::VpnSetting::Ptr &setting, QWidget *parent = nullptr);
~OpenconnectSettingWidget() override;
void loadConfig(const NetworkManager::Setting::Ptr &setting) override;
void loadSecrets(const NetworkManager::Setting::Ptr &setting) override;
QVariantMap setting() const override;
bool isValid() const override;
bool initTokenGroup();
private Q_SLOTS:
void showTokens();
void handleTokenSecret(int index);
void saveTokens();
void restoreTokens();
private:
OpenconnectSettingWidgetPrivate *const d_ptr;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment