Add OAuth2

This commit is contained in:
Julien Wadel 2023-01-31 17:17:37 +01:00
parent 7fde82ab35
commit 67c99eebca
14 changed files with 388 additions and 7 deletions

View file

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- File viewer in chats (Image/Animated Image/Video/Texts) with the option to export the file.
- Accept/decline CLI commands.
- Colored Emojis with its own font family.
- OAuth2 connection to retrieve remote provisioning (Experimental and not usable without configuration).
## 5.0.11 - undefined

View file

@ -94,6 +94,7 @@ endif()
#-------------------------------------------------------------------------------
option(ENABLE_APP_LICENSE "Enable the license in packages." YES)
option(ENABLE_APP_OAUTH2 "Build with OAuth2 support for remote provisioning." OFF) #Experimental.
option(ENABLE_APP_PACKAGING "Enable packaging" NO)
option(ENABLE_APP_PACKAGE_ROOTCA "Embed the rootca file into the package" YES)
option(ENABLE_APP_WEBVIEW "Enable webviews." NO) #Webview is not fully supported because of deployments. Used for subscription.
@ -141,6 +142,7 @@ option(LIBSECRET_SUPPORT "Build with libsecret support" OFF) #Need libsecret-dev
set(APP_OPTIONS "-DENABLE_UPDATE_CHECK=${ENABLE_UPDATE_CHECK}")
list(APPEND APP_OPTIONS "-DENABLE_APP_LICENSE=${ENABLE_APP_LICENSE}")
list(APPEND APP_OPTIONS "-DENABLE_APP_OAUTH2=${ENABLE_APP_OAUTH2}")
list(APPEND APP_OPTIONS "-DENABLE_APP_PACKAGING=${ENABLE_APP_PACKAGING}")
list(APPEND APP_OPTIONS "-DENABLE_APP_PACKAGE_ROOTCA=${ENABLE_APP_PACKAGE_ROOTCA}")
list(APPEND APP_OPTIONS "-DENABLE_APP_WEBVIEW=${ENABLE_APP_WEBVIEW}")
@ -157,6 +159,7 @@ endif()
list(APPEND APP_OPTIONS "-DENABLE_LDAP=${ENABLE_LDAP}")
list(APPEND APP_OPTIONS "-DENABLE_NON_FREE_CODECS=${ENABLE_NON_FREE_CODECS}")
list(APPEND APP_OPTIONS "-DENABLE_OPENH264=${ENABLE_OPENH264}")
list(APPEND APP_OPTIONS "-DENABLE_OPENH264=${ENABLE_OPENH264}")
list(APPEND APP_OPTIONS "-DENABLE_QT_KEYCHAIN=${ENABLE_QT_KEYCHAIN}")
list(APPEND APP_OPTIONS "-DENABLE_OPENSSL_EXPORT=${ENABLE_OPENSSL_EXPORT}")
list(APPEND APP_OPTIONS "-DENABLE_QRCODE=${ENABLE_QRCODE}")

View file

@ -134,6 +134,10 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)#useful for config.h
set(QT5_PACKAGES Core Gui Quick Widgets QuickControls2 Svg LinguistTools Concurrent Network Test Qml)
if(ENABLE_APP_OAUTH2)
list(APPEND QT5_PACKAGES NetworkAuth)
add_definitions(-DENABLE_OAUTH2)
endif()
if(ENABLE_APP_WEBVIEW)
list(APPEND QT5_PACKAGES WebView WebEngine WebEngineCore)
add_definitions(-DENABLE_WEBVIEW)
@ -429,10 +433,12 @@ set(HEADERS
src/utils/Utils.hpp
src/utils/plugins/PluginsManager.hpp
)
set(PLUGIN_HEADERS
include/LinphoneApp/PluginDataAPI.hpp
include/LinphoneApp/PluginNetworkHelper.hpp
include/LinphoneApp/LinphonePlugin.hpp)
list(APPEND SOURCES include/LinphoneApp/PluginExample.json)
set(MAIN_FILE src/app/main.cpp)
@ -483,6 +489,12 @@ if(ENABLE_QT_KEYCHAIN)
list(APPEND SOURCES src/components/vfs/VfsUtils.cpp)
endif()
if(ENABLE_APP_OAUTH2)
list(APPEND HEADERS src/components/authentication/OAuth2Model.hpp)
list(APPEND SOURCES src/components/authentication/OAuth2Model.cpp)
endif()
set(QRC_RESOURCES resources.qrc)
if(ENABLE_APP_WEBVIEW)
set(QRC_WEBVIEW_RESOURCES webview_resources.qrc)

View file

@ -28,6 +28,10 @@
#include "AssistantModel.hpp"
#ifdef ENABLE_OAUTH2
#include "components/authentication/OAuth2Model.hpp"
#endif
#ifdef ENABLE_QRCODE
#include <linphone/FlexiAPIClient.hh>
#endif
@ -166,6 +170,11 @@ AssistantModel::AssistantModel (QObject *parent) : QObject(parent) {
AssistantModel::~AssistantModel(){
setIsReadingQRCode(false);
#ifdef ENABLE_OAUTH2
if(oAuth2Model)
oAuth2Model->deleteLater();
oAuth2Model = nullptr;
#endif
}
// -----------------------------------------------------------------------------
@ -365,6 +374,28 @@ void AssistantModel::attachAccount(const QString& token){
#endif
}
bool AssistantModel::isOAuth2Available(){
#ifdef ENABLE_OAUTH2
return OAuth2Model::isAvailable();
#else
return false;
#endif
}
void AssistantModel::requestOauth2(){
#ifdef ENABLE_OAUTH2
if(isOAuth2Available()){
if(oAuth2Model)
oAuth2Model->deleteLater();
oAuth2Model = new OAuth2Model();
connect(oAuth2Model, &OAuth2Model::requestFailed, this, &AssistantModel::oauth2RequestFailed);
connect(oAuth2Model, &OAuth2Model::statusChanged, this, &AssistantModel::oauth2StatusChanged);
connect(oAuth2Model, &OAuth2Model::authenticated, this, &AssistantModel::oauth2AuthenticationGranted);
oAuth2Model->grant();
}
#endif
}
// -----------------------------------------------------------------------------
QString AssistantModel::getEmail () const {

View file

@ -25,6 +25,9 @@
#include <QObject>
// =============================================================================
#ifdef ENABLE_OAUTH2
class OAuth2Model;
#endif
class AssistantModel : public QObject {
class Handlers;
@ -57,9 +60,12 @@ public:
Q_INVOKABLE void generateQRCode();
Q_INVOKABLE void requestQRCode();
Q_INVOKABLE void readQRCode();
Q_INVOKABLE void requestOauth2();
Q_INVOKABLE void attachAccount(const QString& token);
Q_INVOKABLE static bool isOAuth2Available();
void checkLinkingAccount();
public slots:
@ -80,6 +86,10 @@ signals:
void createStatusChanged (const QString &error);
void loginStatusChanged (const QString &error);
void recoverStatusChanged (const QString &error);
void oauth2RequestFailed(const QString& error);
void oauth2StatusChanged(const QString& status);
void oauth2AuthenticationGranted();
void configFilenameChanged (const QString &configFilename);
@ -132,6 +142,9 @@ private:
std::shared_ptr<linphone::AccountCreator> mAccountCreator;
std::shared_ptr<Handlers> mHandlers;
#ifdef ENABLE_OAUTH2
OAuth2Model * oAuth2Model = nullptr;
#endif
};
#endif // ASSISTANT_MODEL_H_

View file

@ -0,0 +1,160 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* 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, either version 3 of the License, or
* (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "OAuth2Model.hpp"
#include <QtNetworkAuth>
#include <QDesktopServices>
#include "components/core/CoreManager.hpp"
#include "components/settings/SettingsModel.hpp"
#include "utils/Utils.hpp"
// =============================================================================
// Goal : override the default redirect url which is "http://localhost:0"
class OAuthHttpServerReplyHandler : public QOAuthHttpServerReplyHandler{
public:
OAuthHttpServerReplyHandler(const int& port, QObject * parent = nullptr ) : QOAuthHttpServerReplyHandler(port, parent){
}
QString callback() const override{
QString uri = CoreManager::getInstance()->getSettingsModel()->getOAuth2RedirectUri();
if( uri!= "")
return uri;
else
return QOAuthHttpServerReplyHandler::callback();// Return default
}
};
OAuth2Model::OAuth2Model (QObject *parent){
QUrl url(CoreManager::getInstance()->getSettingsModel()->getOAuth2RedirectUri());
auto replyHandler = new OAuthHttpServerReplyHandler(url.port(0), this);
oauth2.setReplyHandler(replyHandler);
oauth2.setAuthorizationUrl(QUrl(CoreManager::getInstance()->getSettingsModel()->getOAuth2AuthorizationUrl()));
oauth2.setAccessTokenUrl(QUrl(CoreManager::getInstance()->getSettingsModel()->getOAuth2AccessTokenUrl()));
oauth2.setNetworkAccessManager(new QNetworkAccessManager(&oauth2));
oauth2.setClientIdentifier(CoreManager::getInstance()->getSettingsModel()->getOAuth2Identifier());
oauth2.setClientIdentifierSharedKey(CoreManager::getInstance()->getSettingsModel()->getOAuth2Password());
oauth2.setScope(CoreManager::getInstance()->getSettingsModel()->getOAuth2Scope());
connect(oauth2.networkAccessManager(), &QNetworkAccessManager::authenticationRequired, [=](QNetworkReply *reply, QAuthenticator *authenticator){
qWarning() << "authenticationRequired received but not implemented";
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::statusChanged, [=](QAbstractOAuth::Status status) {
qWarning() << (int)status;
switch(status){
case QAbstractOAuth::Status::Granted : {
emit statusChanged("Authentication granted");
emit authenticated();
break;
}
case QAbstractOAuth::Status::NotAuthenticated : {
emit statusChanged("Not authenticated");
break;
}
case QAbstractOAuth::Status::RefreshingToken : {
emit statusChanged("Refreshing token");
break;
}
case QAbstractOAuth::Status::TemporaryCredentialsReceived : {
emit statusChanged("Temporary credentials received");
break;
}
default:{}
}
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, [=](QAbstractOAuth::Error error){
qWarning() << (int)error;
switch(error){
case QAbstractOAuth::Error::NetworkError : emit requestFailed("Network error"); break;
case QAbstractOAuth::Error::ServerError : emit requestFailed("Server error"); break;
case QAbstractOAuth::Error::OAuthTokenNotFoundError : emit requestFailed("OAuth token not found"); break;
case QAbstractOAuth::Error::OAuthTokenSecretNotFoundError : emit requestFailed("OAuth token secret not found"); break;
case QAbstractOAuth::Error::OAuthCallbackNotVerified: emit requestFailed("OAuth callback not verified"); break;
default:{
}
}
});
// in case we want to add parameters.
oauth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QVariantMap *parameters) {
qWarning() << (int)stage;
switch(stage){
case QAbstractOAuth::Stage::RequestingAccessToken : {
emit statusChanged("Requesting access token");
break;
}
case QAbstractOAuth::Stage::RefreshingAccessToken : {
emit statusChanged("Refreshing access token");
break;
}
case QAbstractOAuth::Stage::RequestingAuthorization : {
emit statusChanged("Requesting authorization");
break;
}
case QAbstractOAuth::Stage::RequestingTemporaryCredentials : {
emit statusChanged("Requesting temporary credentials");
break;
}
default:{}
}
});
//
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, [this](const QUrl & url){
qDebug() << "Browser authentication url : " << url;
emit statusChanged("Requesting authorization from browser");
QDesktopServices::openUrl(url);
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::finished, [this](QNetworkReply * reply){
qDebug() << "Finished " << reply->errorString();
connect(reply, &QNetworkReply::errorOccurred, [this, reply](QNetworkReply::NetworkError error){
qDebug() << reply->errorString();
});
});
connect(this, &OAuth2Model::authenticated, this, &OAuth2Model::getRemoteProvisioning);
}
bool OAuth2Model::isAvailable(){
return !CoreManager::getInstance()->getSettingsModel()->getOAuth2AuthorizationUrl().isEmpty()
&& !CoreManager::getInstance()->getSettingsModel()->getOAuth2AccessTokenUrl().isEmpty();
}
void OAuth2Model::grant(){
oauth2.grant();
}
void OAuth2Model::getRemoteProvisioning() {
qDebug() << "getRemoteProvisioning " << oauth2.extraTokens() << oauth2.token();
QString basicAuthentication(CoreManager::getInstance()->getSettingsModel()->getOAuth2RemoteProvisioningBasicAuth());
QUrl url(CoreManager::getInstance()->getSettingsModel()->getRemoteProvisioningRootUrl());
std::vector<std::pair<std::string, std::string> > headers;
auto header = CoreManager::getInstance()->getSettingsModel()->getOAuth2RemoteProvisioningHeader();
if( !header.isEmpty())
headers.push_back(std::pair<std::string, std::string>(Utils::appStringToCoreString(header), Utils::appStringToCoreString(oauth2.token())));
headers.push_back(std::pair<std::string, std::string>("accept", "application/xml"));
if(!basicAuthentication.isEmpty()){
headers.push_back(std::pair<std::string, std::string>("authorization", Utils::appStringToCoreString("Basic "+basicAuthentication)));
}
CoreManager::getInstance()->getCore()->clearProvisioningHeaders();
for(int i = 0 ; i < headers.size() ; ++i)
CoreManager::getInstance()->getCore()->addProvisioningHeader(headers[i].first, headers[i].second);
CoreManager::getInstance()->getSettingsModel()->setRemoteProvisioning(url.toString());
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* 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, either version 3 of the License, or
* (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef O_AUTH_2_MODEL_H_
#define O_AUTH_2_MODEL_H_
#include <memory>
#include <QObject>
#include <QOAuth2AuthorizationCodeFlow>
// =============================================================================
class OAuth2Model : public QObject {
Q_OBJECT
public:
OAuth2Model (QObject *parent = Q_NULLPTR);
void grant(); //Start authentication
void getRemoteProvisioning();
static bool isAvailable();
signals:
void authenticated();
void requestFailed(const QString& error);
void statusChanged(const QString& status);
void remoteProvisioningReceived(const QString& url);
private:
QOAuth2AuthorizationCodeFlow oauth2;
};
#endif

View file

@ -32,6 +32,7 @@
#include "app/logger/Logger.hpp"
#include "app/paths/Paths.hpp"
#include "components/assistant/AssistantModel.hpp"
#include "components/core/CoreManager.hpp"
#include "components/tunnel/TunnelModel.hpp"
#include "include/LinphoneApp/PluginNetworkHelper.hpp"
@ -1756,6 +1757,58 @@ bool SettingsModel::isLdapAvailable(){
return CoreManager::getInstance()->getCore()->ldapAvailable();
}
bool SettingsModel::isOAuth2Available(){
return AssistantModel::isOAuth2Available();
}
QString SettingsModel::getOAuth2AuthorizationUrl()const{
return Utils::coreStringToAppString(
mConfig->getString(UiSection, "oauth2_authorization_url", Constants::OAuth2AuthorizationUrl)
);
}
QString SettingsModel::getOAuth2AccessTokenUrl()const{
return Utils::coreStringToAppString(
mConfig->getString(UiSection, "oauth2_access_token_url", Constants::OAuth2AccessTokenUrl)
);
}
QString SettingsModel::getOAuth2RedirectUri()const{
return Utils::coreStringToAppString(
mConfig->getString(UiSection, "oauth2_redirect_uri", Constants::OAuth2RedirectUri)
);
}
QString SettingsModel::getOAuth2Identifier()const{
return Utils::coreStringToAppString(
mConfig->getString(UiSection, "oauth2_identifier", Constants::OAuth2Identifier)
);
}
QString SettingsModel::getOAuth2Password()const{
return Utils::coreStringToAppString(
mConfig->getString(UiSection, "oauth2_password", Constants::OAuth2Password)
);
}
QString SettingsModel::getOAuth2Scope()const{
return Utils::coreStringToAppString(
mConfig->getString(UiSection, "oauth2_scope", Constants::OAuth2Scope)
);
}
QString SettingsModel::getOAuth2RemoteProvisioningBasicAuth()const{
return Utils::coreStringToAppString(
mConfig->getString(UiSection, "oauth2_remote_provisioning_basic_auth", Constants::RemoteProvisioningBasicAuth)
);
}
QString SettingsModel::getOAuth2RemoteProvisioningHeader()const{
return Utils::coreStringToAppString(
mConfig->getString(UiSection, "oauth2_remote_provisioning_header", Constants::DefaultOAuth2RemoteProvisioningHeader)
);
}
// ---------------------------------------------------------------------------
QString SettingsModel::getLogsFolder (const shared_ptr<linphone::Config> &config) {

View file

@ -630,6 +630,17 @@ public:
Q_INVOKABLE bool isLdapAvailable();
// OAuth 2
Q_INVOKABLE bool isOAuth2Available();
QString getOAuth2AuthorizationUrl()const;
QString getOAuth2AccessTokenUrl()const;
QString getOAuth2RedirectUri()const;
QString getOAuth2Identifier()const;
QString getOAuth2Password()const;
QString getOAuth2Scope()const;
QString getOAuth2RemoteProvisioningBasicAuth()const;
QString getOAuth2RemoteProvisioningHeader()const;
// ---------------------------------------------------------------------------
static QString getLogsFolder (const std::shared_ptr<linphone::Config> &config);

View file

@ -76,6 +76,17 @@ constexpr char Constants::DefaultAssistantRegistrationUrl[];
constexpr char Constants::DefaultAssistantLoginUrl[];
constexpr char Constants::DefaultAssistantLogoutUrl[];
constexpr char Constants::RemoteProvisioningBasicAuth[];
constexpr char Constants::OAuth2AuthorizationUrl[];
constexpr char Constants::OAuth2AccessTokenUrl[];
constexpr char Constants::OAuth2RedirectUri[];
constexpr char Constants::OAuth2Identifier[];
constexpr char Constants::OAuth2Password[];
constexpr char Constants::OAuth2Scope[];
constexpr char Constants::DefaultOAuth2RemoteProvisioningHeader[];
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
constexpr char Constants::H264Description[];
#endif // if defined(Q_OS_LINUX) || defined(Q_OS_WIN)

View file

@ -67,8 +67,18 @@ public:
static constexpr char DefaultRlsUri[] = "sips:rls@sip.linphone.org";
static constexpr char DefaultLogsEmail[] = "linphone-desktop@belledonne-communications.com";
static constexpr char DefaultFlexiAPIURL[] = "http://fs-test-sandbox.linphone.org/flexiapi/api/";// Need "/" at the end
static constexpr char RemoteProvisioningURL[] = "http://fs-test-sandbox.linphone.org/flexiapi/provisioning";
static constexpr char DefaultFlexiAPIURL[] = "https://subscribe.linphone.org/api/";// Need "/" at the end
static constexpr char RemoteProvisioningURL[] = "https://subscribe.linphone.org/api/provisioning";
static constexpr char RemoteProvisioningBasicAuth[] = "";
// OAuth2 settings
static constexpr char OAuth2AuthorizationUrl[] = "";
static constexpr char OAuth2AccessTokenUrl[] = "";
static constexpr char OAuth2RedirectUri[] = "";
static constexpr char OAuth2Identifier[] = "";
static constexpr char OAuth2Password[] = "";
static constexpr char OAuth2Scope[] = "";
static constexpr char DefaultOAuth2RemoteProvisioningHeader[] = "x-linphone-oauth2-token";
Q_PROPERTY(QString PasswordRecoveryUrl MEMBER PasswordRecoveryUrl CONSTANT)
Q_PROPERTY(QString CguUrl MEMBER CguUrl CONSTANT)

View file

@ -1,6 +1,7 @@
import QtQuick 2.7
import Common 1.0
import Common.Styles 1.0
import Linphone.Styles 1.0
// =============================================================================
@ -50,7 +51,7 @@ Column {
padding: RequestBlockStyle.error.padding
wrapMode: Text.WordWrap
visible: !block.loading && errorBlock.text != ''
visible: errorBlock.text != ''
}
BusyIndicator {
@ -58,7 +59,7 @@ Column {
anchors {
horizontalCenter: parent.horizontalCenter
}
color: BusyIndicatorStyle.alternateColor.color
height: visible ? RequestBlockStyle.loadingIndicator.height : 0
width: RequestBlockStyle.loadingIndicator.width

View file

@ -44,6 +44,9 @@ Item{
target: assistantModel
onNewQRCodeReceived: {assistantModel.qrcode = 'image://qrcode/'+code; requestBlock.stop('')}
onNewQRCodeNotReceived: requestBlock.stop(message)
onOauth2StatusChanged: requestBlock.setText(status)
onOauth2RequestFailed: requestBlock.stop(error)
onOauth2AuthenticationGranted: requestBlock.stop('')
onProvisioningTokenReceived: {url.text = token
SettingsModel.remoteProvisioning = url.text
assistantModel.qrcode = ''
@ -116,8 +119,27 @@ Item{
id: requestBlock
action: (function () {
})
Layout.preferredWidth: parent.width
Layout.fillWidth: true
}
Text{
Layout.topMargin: 15
Layout.alignment: Qt.AlignCenter
visible: oAuth2Button.visible
font.pointSize: FetchRemoteConfigurationStyle.fieldTitles.pointSize
font.weight: Font.Bold
font.capitalization: Font.Capitalize
color: FetchRemoteConfigurationStyle.fieldTitles.colorModel.color
//: 'or' : conjunction to choose between options.
text: qsTr('or')
}
TextButtonB {
id: oAuth2Button
Layout.margins: 15
Layout.alignment: Qt.AlignCenter
text: 'OAuth2'
visible: assistantModel.isOAuth2Available()
onClicked: {requestBlock.execute(); assistantModel.requestOauth2()}
capitalization: Font.AllUppercase
}
Text{
Layout.topMargin: 15
@ -192,6 +214,7 @@ Item{
capitalization: Font.AllUppercase
}
}
//------------------------------------------------------------------
// Developer Section
//------------------------------------------------------------------

@ -1 +1 @@
Subproject commit acfe74e4f769606b7f09819430c79836362cf19c
Subproject commit 05d1ffdf582f459e801cf726837fb091cba595e0