Skip to content

Commit d56fc88

Browse files
committedJul 19, 2018
[oauth] Client registration with JWT
Ported from https://github.com/securedimensions/QGIS-OAuth2-Plugin The Testbed 13 version provides an additional configuration tab "software statement" which allows a user to automatically register the plugin with a required configuration with the Authorization Server. Of course this can only be leveraged, if the Authorization Server involved supports the registration via digitally signed software statements (JWTs) as described in this ER.
1 parent c50e99e commit d56fc88

12 files changed

+510
-7
lines changed
 

‎src/auth/oauth2/qgsauthoauth2edit.cpp

Lines changed: 239 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
#include "qgsauthguiutils.h"
2323
#include "qgsauthmanager.h"
2424
#include "qgsauthconfigedit.h"
25-
#include "qgslogger.h"
26-
25+
#include "qgsmessagelog.h"
26+
#include "qgsnetworkaccessmanager.h"
27+
#include "qjsonwrapper/Json.h"
2728

2829
QgsAuthOAuth2Edit::QgsAuthOAuth2Edit( QWidget *parent )
2930
: QgsAuthMethodEdit( parent )
@@ -146,6 +147,17 @@ void QgsAuthOAuth2Edit::setupConnections()
146147
connect( btnGetDefinedDirPath, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::getDefinedCustomDir );
147148
connect( leDefinedDirPath, &QLineEdit::textChanged, this, &QgsAuthOAuth2Edit::definedCustomDirChanged );
148149

150+
connect( btnSoftStatementDir, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::getSoftStatementDir );
151+
connect( leSoftwareStatementJwtPath, &QLineEdit::textChanged,this, &QgsAuthOAuth2Edit::softwareStatementJwtPathChanged );
152+
connect( leSoftwareStatementConfigUrl, &QLineEdit::textChanged, [ = ] ( const QString &txt ) {
153+
btnRegister->setEnabled( QUrl( txt ).isValid() && ! leSoftwareStatementJwtPath->text().isEmpty() );
154+
});
155+
connect( btnRegister, &QPushButton::clicked, this, &QgsAuthOAuth2Edit::getSoftwareStatementConfig );
156+
157+
// FIXME: in the testbed13 code this signal does not exists (but a connection was attempted)
158+
//connect( this, &QgsAuthOAuth2Edit::configSucceeded, this, &QgsAuthOAuth2Edit::registerSoftStatement );
159+
160+
149161
// Custom config editing connections
150162
connect( cmbbxGrantFlow, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
151163
this, &QgsAuthOAuth2Edit::updateGrantFlow ); // also updates GUI
@@ -353,7 +365,6 @@ void QgsAuthOAuth2Edit::clearConfig()
353365
loadFromOAuthConfig( mOAuthConfigCustom.get() );
354366
}
355367

356-
// slot
357368
void QgsAuthOAuth2Edit::loadFromOAuthConfig( const QgsAuthOAuth2Config *config )
358369
{
359370
if ( !config )
@@ -494,6 +505,21 @@ void QgsAuthOAuth2Edit::definedCustomDirChanged( const QString &path )
494505
}
495506
}
496507

508+
509+
void QgsAuthOAuth2Edit::softwareStatementJwtPathChanged( const QString &path )
510+
{
511+
QFileInfo pinfo( path );
512+
bool ok = pinfo.exists() || pinfo.isFile();
513+
514+
leSoftwareStatementJwtPath->setStyleSheet( ok ? "" : QgsAuthGuiUtils::redTextStyleSheet() );
515+
516+
if ( ok )
517+
{
518+
parseSoftwareStatement( path );
519+
}
520+
}
521+
522+
497523
// slot
498524
void QgsAuthOAuth2Edit::setCurrentDefinedConfig( const QString &id )
499525
{
@@ -557,6 +583,20 @@ void QgsAuthOAuth2Edit::getDefinedCustomDir()
557583
leDefinedDirPath->setText( extradir );
558584
}
559585

586+
void QgsAuthOAuth2Edit::getSoftStatementDir()
587+
{
588+
QString softStatementFile = QFileDialog::getOpenFileName( this, tr( "Select software statement file" ),
589+
QDir::homePath(), tr( "JSON Web Token (*.jwt)") );
590+
this->raise();
591+
this->activateWindow();
592+
593+
if ( softStatementFile.isNull() )
594+
{
595+
return;
596+
}
597+
leSoftwareStatementJwtPath->setText( softStatementFile );
598+
}
599+
560600
void QgsAuthOAuth2Edit::initConfigObjs()
561601
{
562602
mOAuthConfigCustom = qgis::make_unique<QgsAuthOAuth2Config>( nullptr );
@@ -673,6 +713,11 @@ bool QgsAuthOAuth2Edit::onDefinedTab() const
673713
return mCurTab == definedTab();
674714
}
675715

716+
bool QgsAuthOAuth2Edit::onStatementTab() const
717+
{
718+
return mCurTab == statementTab();
719+
}
720+
676721
// slot
677722
void QgsAuthOAuth2Edit::updateGrantFlow( int indx )
678723
{
@@ -910,3 +955,194 @@ void QgsAuthOAuth2Edit::clearQueryPairs()
910955
tblwdgQueryPairs->removeRow( i - 1 );
911956
}
912957
}
958+
959+
void QgsAuthOAuth2Edit::parseSoftwareStatement(const QString& path)
960+
{
961+
QFile file(path);
962+
QByteArray softwareStatementBase64;
963+
if(file.open(QIODevice::ReadOnly | QIODevice::Text))
964+
{
965+
softwareStatementBase64=file.readAll();
966+
}
967+
if(softwareStatementBase64.isEmpty())
968+
{
969+
QgsDebugMsg( QStringLiteral( "Error software statement is empty: %1" ).arg( QString( path ) ) );
970+
file.close();
971+
return;
972+
}
973+
file.close();
974+
mSoftwareStatement.insert("software_statement",softwareStatementBase64);
975+
QByteArray payload=softwareStatementBase64.split('.')[1];
976+
QByteArray decoded=QByteArray::fromBase64(payload/*, QByteArray::Base64UrlEncoding*/);
977+
QByteArray errStr;
978+
bool res = false;
979+
QMap<QString, QVariant> jsonData = QJsonWrapper::parseJson(decoded, &res, &errStr).toMap();
980+
if ( !res )
981+
{
982+
QgsDebugMsg( QStringLiteral( "Error parsing JSON: %1" ).arg( QString( errStr ) ));
983+
return;
984+
}
985+
if(jsonData.contains("grant_types") && jsonData.contains( QLatin1Literal( "redirect_uris" ) ) )
986+
{
987+
QString grantType = jsonData[QLatin1Literal ( "grant_types" ) ].toStringList()[0];
988+
if(grantType == QLatin1Literal( "authorization_code" ) )
989+
{
990+
updateGrantFlow( static_cast<int>( QgsAuthOAuth2Config::AuthCode ) );
991+
}
992+
else
993+
{
994+
updateGrantFlow( static_cast<int>( QgsAuthOAuth2Config::ResourceOwner ) );
995+
}
996+
//Set redirect_uri
997+
QString redirectUri = jsonData[QLatin1Literal( "redirect_uris" ) ].toStringList()[0];
998+
leRedirectUrl->setText(redirectUri);
999+
}
1000+
else
1001+
{
1002+
QgsDebugMsgLevel( QStringLiteral( "Error software statement is invalid: %1" ).arg( QString( path ) ), 4 );
1003+
return;
1004+
}
1005+
if(jsonData.contains(QLatin1Literal( "registration_endpoint")) )
1006+
{
1007+
mRegistrationEndpoint = jsonData[QLatin1Literal("registration_endpoint")].toString();
1008+
leSoftwareStatementConfigUrl->setText( mRegistrationEndpoint );
1009+
}
1010+
QgsDebugMsgLevel( QStringLiteral( "JSON: %1" ).arg( QString::fromLocal8Bit( decoded.data() ) ), 4 );
1011+
}
1012+
1013+
void QgsAuthOAuth2Edit::configReplyFinished()
1014+
{
1015+
qDebug() << "QgsAuthOAuth2Edit::onConfigReplyFinished";
1016+
QNetworkReply *configReply = qobject_cast<QNetworkReply *>(sender());
1017+
if (configReply->error() == QNetworkReply::NoError)
1018+
{
1019+
QByteArray replyData = configReply->readAll();
1020+
QByteArray errStr;
1021+
bool res = false;
1022+
QVariantMap config = QJsonWrapper::parseJson(replyData, &res, &errStr).toMap();
1023+
1024+
if ( !res )
1025+
{
1026+
QgsDebugMsg( QStringLiteral( "Error parsing JSON: %1" ).arg( QString( errStr ) ) );
1027+
return;
1028+
}
1029+
// I haven't found any docs about the content of this confg JSON file
1030+
// I assume that registration_endpoint is all that it contains
1031+
// But we also might have other optional information here
1032+
if(config.contains(QLatin1Literal( "registration_endpoint")) )
1033+
{
1034+
if ( config.contains(QLatin1Literal("authorization_endpoint" ) ) )
1035+
leRequestUrl->setText(config.value(QLatin1Literal("authorization_endpoint" ) ).toString());
1036+
if ( config.contains(QLatin1Literal("token_endpoint" ) ) )
1037+
leTokenUrl->setText(config.value(QLatin1Literal("token_endpoint" ) ).toString());
1038+
1039+
registerSoftStatement(config.value(QLatin1Literal("registration_endpoint")).toString());
1040+
}
1041+
else
1042+
{
1043+
QString errorMsg = QStringLiteral( "Downloading configuration failed with error: %1" ).arg( configReply->errorString() );
1044+
QgsMessageLog::logMessage( errorMsg, QStringLiteral( "OAuth2" ), Qgis::Critical );
1045+
}
1046+
}
1047+
mDownloading = false;
1048+
configReply->deleteLater();
1049+
}
1050+
1051+
void QgsAuthOAuth2Edit::registerReplyFinished()
1052+
{
1053+
//JSV todo
1054+
//better error handling
1055+
qDebug() << "QgsAuthOAuth2Edit::onRegisterReplyFinished";
1056+
QNetworkReply *registerReply = qobject_cast<QNetworkReply *>(sender());
1057+
if (registerReply->error() == QNetworkReply::NoError)
1058+
{
1059+
QByteArray replyData = registerReply->readAll();
1060+
QByteArray errStr;
1061+
bool res = false;
1062+
QVariantMap clientInfo = QJsonWrapper::parseJson(replyData, &res, &errStr).toMap();
1063+
1064+
// According to RFC 7591 sec. 3.2.1. Client Information Response the only
1065+
// required field is client_id
1066+
leClientId->setText(clientInfo.value(QLatin1Literal("client_id" ) ).toString());
1067+
if ( clientInfo.contains(QLatin1Literal("client_secret" )) )
1068+
leClientSecret->setText(clientInfo.value(QLatin1Literal("client_secret" ) ).toString());
1069+
if ( clientInfo.contains(QLatin1Literal("authorization_endpoint" ) ) )
1070+
leRequestUrl->setText(clientInfo.value(QLatin1Literal("authorization_endpoint" ) ).toString());
1071+
if ( clientInfo.contains(QLatin1Literal("token_endpoint" ) ) )
1072+
leTokenUrl->setText(clientInfo.value(QLatin1Literal("token_endpoint" ) ).toString());
1073+
if ( clientInfo.contains(QLatin1Literal("scopes" ) ) )
1074+
leScope->setText(clientInfo.value(QLatin1Literal("scopes" ) ).toString());
1075+
1076+
tabConfigs->setCurrentIndex(0);
1077+
}
1078+
else
1079+
{
1080+
QString errorMsg = QStringLiteral( "Client registration failed with error: %1" ).arg( registerReply->errorString() );
1081+
QgsMessageLog::logMessage( errorMsg, QLatin1Literal( "OAuth2" ) , Qgis::Critical);
1082+
}
1083+
mDownloading = false;
1084+
registerReply->deleteLater();
1085+
}
1086+
1087+
void QgsAuthOAuth2Edit::networkError(QNetworkReply::NetworkError error)
1088+
{
1089+
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
1090+
qWarning() << "QgsAuthOAuth2Edit::onNetworkError: " << error << ": " << reply->errorString();
1091+
QString errorMsg = QStringLiteral( "Network error: %1" ).arg( reply->errorString() );
1092+
QgsMessageLog::logMessage( errorMsg, QLatin1Literal( "OAuth2" ), Qgis::Critical );
1093+
qDebug() << "QgsAuthOAuth2Edit::onNetworkError: " << reply->readAll();
1094+
}
1095+
1096+
1097+
void QgsAuthOAuth2Edit::registerSoftStatement(const QString& registrationUrl)
1098+
{
1099+
QUrl regUrl(registrationUrl);
1100+
if( !regUrl.isValid() )
1101+
{
1102+
qWarning()<<"Registration url is not valid";
1103+
return;
1104+
}
1105+
QByteArray errStr;
1106+
bool res = false;
1107+
QByteArray json = QJsonWrapper::toJson(QVariant(mSoftwareStatement),&res,&errStr);
1108+
QNetworkRequest registerRequest(regUrl);
1109+
registerRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1Literal( "application/json") );
1110+
QNetworkReply * registerReply;
1111+
// For testability: use GET if protocol is file://
1112+
if ( regUrl.scheme() == QLatin1Literal( "file" ) )
1113+
registerReply = QgsNetworkAccessManager::instance()->get(registerRequest);
1114+
else
1115+
registerReply = QgsNetworkAccessManager::instance()->post(registerRequest, json);
1116+
mDownloading = true;
1117+
connect(registerReply, &QNetworkReply::finished, this, &QgsAuthOAuth2Edit::registerReplyFinished, Qt::QueuedConnection);
1118+
connect(registerReply, qgis::overload<QNetworkReply::NetworkError>::of( &QNetworkReply::error ), this, &QgsAuthOAuth2Edit::networkError, Qt::QueuedConnection);
1119+
}
1120+
1121+
void QgsAuthOAuth2Edit::getSoftwareStatementConfig()
1122+
{
1123+
if(!mRegistrationEndpoint.isEmpty())
1124+
{
1125+
registerSoftStatement(mRegistrationEndpoint);
1126+
}
1127+
else
1128+
{
1129+
QString config = leSoftwareStatementConfigUrl->text();
1130+
QUrl configUrl(config);
1131+
QNetworkRequest configRequest(configUrl);
1132+
QNetworkReply * configReply = QgsNetworkAccessManager::instance()->get(configRequest);
1133+
mDownloading = true;
1134+
connect(configReply, &QNetworkReply::finished, this, &QgsAuthOAuth2Edit::configReplyFinished, Qt::QueuedConnection);
1135+
connect(configReply, qgis::overload<QNetworkReply::NetworkError>::of( &QNetworkReply::error ), this, &QgsAuthOAuth2Edit::networkError, Qt::QueuedConnection);
1136+
}
1137+
}
1138+
1139+
QString QgsAuthOAuth2Edit::registrationEndpoint() const
1140+
{
1141+
return mRegistrationEndpoint;
1142+
}
1143+
1144+
void QgsAuthOAuth2Edit::setRegistrationEndpoint(const QString& registrationEndpoint)
1145+
{
1146+
mRegistrationEndpoint = registrationEndpoint;
1147+
}
1148+

‎src/auth/oauth2/qgsauthoauth2edit.h

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#define QGSAUTHOAUTH2EDIT_H
1717

1818
#include <QWidget>
19+
#include <QNetworkReply>
1920
#include "qgsauthmethodedit.h"
2021
#include "ui_qgsauthoauth2edit.h"
2122

@@ -50,6 +51,7 @@ class QgsAuthOAuth2Edit : public QgsAuthMethodEdit, private Ui::QgsAuthOAuth2Edi
5051
*/
5152
QgsStringMap configMap() const override;
5253

54+
5355
public slots:
5456

5557
//! Load the configuration from \a configMap
@@ -69,30 +71,42 @@ class QgsAuthOAuth2Edit : public QgsAuthMethodEdit, private Ui::QgsAuthOAuth2Edi
6971
void removeTokenCacheFile();
7072

7173
void populateGrantFlows();
74+
7275
void updateGrantFlow( int indx );
7376

7477
void exportOAuthConfig();
78+
7579
void importOAuthConfig();
7680

7781
void descriptionChanged();
7882

7983
void populateAccessMethods();
84+
8085
void updateConfigAccessMethod( int indx );
8186

8287
void addQueryPair();
88+
8389
void removeQueryPair();
90+
8491
void clearQueryPairs();
92+
8593
void populateQueryPairs( const QVariantMap &querypairs, bool append = false );
94+
8695
void queryTableSelectionChanged();
96+
8797
void updateConfigQueryPairs();
8898

8999
void updateDefinedConfigsCache();
100+
90101
void loadDefinedConfigs();
102+
91103
void setCurrentDefinedConfig( const QString &id );
104+
92105
void currentDefinedItemChanged( QListWidgetItem *cur, QListWidgetItem *prev );
106+
93107
void selectCurrentDefinedConfig();
94108

95-
void loadFromOAuthConfig( const QgsAuthOAuth2Config *config = nullptr );
109+
void getSoftStatementDir();
96110

97111
void updateTokenCacheFile( bool curpersist ) const;
98112

@@ -102,8 +116,26 @@ class QgsAuthOAuth2Edit : public QgsAuthMethodEdit, private Ui::QgsAuthOAuth2Edi
102116

103117
void getDefinedCustomDir();
104118

119+
void loadFromOAuthConfig( const QgsAuthOAuth2Config *config );
120+
121+
void softwareStatementJwtPathChanged( const QString &path );
122+
123+
void configReplyFinished();
124+
125+
void registerReplyFinished();
126+
127+
void networkError(QNetworkReply::NetworkError error);
128+
129+
//! For testability
130+
QString registrationEndpoint() const;
131+
132+
//! For testability
133+
void setRegistrationEndpoint(const QString& registrationEndpoint);
134+
105135
private:
136+
106137
void initGui();
138+
void parseSoftwareStatement(const QString& path);
107139

108140
QWidget *parentWidget() const;
109141
QLineEdit *parentNameField() const;
@@ -118,8 +150,11 @@ class QgsAuthOAuth2Edit : public QgsAuthMethodEdit, private Ui::QgsAuthOAuth2Edi
118150

119151
int customTab() const { return 0; }
120152
int definedTab() const { return 1; }
153+
int statementTab() const { return 2; }
121154
bool onCustomTab() const;
122155
bool onDefinedTab() const;
156+
bool onStatementTab() const;
157+
void getSoftwareStatementConfig();
123158

124159
QString currentDefinedConfig() const { return mDefinedId; }
125160

@@ -132,6 +167,11 @@ class QgsAuthOAuth2Edit : public QgsAuthMethodEdit, private Ui::QgsAuthOAuth2Edi
132167
int mCurTab = 0;
133168
bool mPrevPersistToken = false;
134169
QToolButton *btnTokenClear = nullptr;
170+
QString mRegistrationEndpoint;
171+
QMap<QString, QVariant> mSoftwareStatement;
172+
void registerSoftStatement(const QString& registrationUrl);
173+
bool mDownloading = false;
174+
friend class TestQgsAuthOAuth2Method;
135175
};
136176

137177
#endif // QGSAUTHOAUTH2EDIT_H

‎src/auth/oauth2/qgsauthoauth2edit.ui

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
</sizepolicy>
7676
</property>
7777
<property name="currentIndex">
78-
<number>0</number>
78+
<number>2</number>
7979
</property>
8080
<widget class="QWidget" name="tabCustom">
8181
<attribute name="title">
@@ -202,7 +202,7 @@
202202
<property name="geometry">
203203
<rect>
204204
<x>0</x>
205-
<y>-241</y>
205+
<y>0</y>
206206
<width>401</width>
207207
<height>516</height>
208208
</rect>
@@ -734,6 +734,85 @@
734734
</item>
735735
</layout>
736736
</widget>
737+
<widget class="QWidget" name="tabStatement">
738+
<attribute name="title">
739+
<string>Software Statement</string>
740+
</attribute>
741+
<layout class="QVBoxLayout" name="verticalLayout_7">
742+
<property name="spacing">
743+
<number>6</number>
744+
</property>
745+
<property name="sizeConstraint">
746+
<enum>QLayout::SetDefaultConstraint</enum>
747+
</property>
748+
<item>
749+
<layout class="QGridLayout" name="gridLayout_4">
750+
<item row="1" column="0">
751+
<widget class="QLabel" name="lblSoftStatementDir">
752+
<property name="text">
753+
<string>Software Statement</string>
754+
</property>
755+
</widget>
756+
</item>
757+
<item row="0" column="1">
758+
<widget class="QLineEdit" name="leSoftwareStatementConfigUrl">
759+
<property name="placeholderText">
760+
<string>Optional</string>
761+
</property>
762+
</widget>
763+
</item>
764+
<item row="1" column="2">
765+
<widget class="QToolButton" name="btnSoftStatementDir">
766+
<property name="text">
767+
<string>...</string>
768+
</property>
769+
<property name="icon">
770+
<iconset resource="oauth2_resources.qrc">
771+
<normaloff>:/oauth2method/oauth2_resources/fileopen.svg</normaloff>:/oauth2method/oauth2_resources/fileopen.svg</iconset>
772+
</property>
773+
</widget>
774+
</item>
775+
<item row="0" column="0">
776+
<widget class="QLabel" name="lblConfigUrl">
777+
<property name="text">
778+
<string>Configuration Url</string>
779+
</property>
780+
</widget>
781+
</item>
782+
<item row="1" column="1">
783+
<widget class="QLineEdit" name="leSoftwareStatementJwtPath">
784+
<property name="placeholderText">
785+
<string>Required</string>
786+
</property>
787+
</widget>
788+
</item>
789+
<item row="2" column="1">
790+
<widget class="QPushButton" name="btnRegister">
791+
<property name="enabled">
792+
<bool>false</bool>
793+
</property>
794+
<property name="text">
795+
<string>Register</string>
796+
</property>
797+
</widget>
798+
</item>
799+
</layout>
800+
</item>
801+
<item>
802+
<spacer name="verticalSpacer_2">
803+
<property name="orientation">
804+
<enum>Qt::Vertical</enum>
805+
</property>
806+
<property name="sizeHint" stdset="0">
807+
<size>
808+
<width>20</width>
809+
<height>40</height>
810+
</size>
811+
</property>
812+
</spacer>
813+
</item>
814+
</layout>
815+
</widget>
737816
</widget>
738817
<widget class="QgsCollapsibleGroupBoxBasic" name="grpbxAdvanced">
739818
<property name="sizePolicy">

‎src/auth/oauth2/qgsauthoauth2method.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "qgssettings.h"
3030

3131
#include <QDateTime>
32+
#include <QInputDialog>
3233
#include <QDesktopServices>
3334
#include <QDir>
3435
#include <QEventLoop>
@@ -173,7 +174,8 @@ bool QgsAuthOAuth2Method::updateNetworkRequest( QNetworkRequest &request, const
173174
connect( o2, &QgsO2::linkingSucceeded, this, &QgsAuthOAuth2Method::onLinkingSucceeded, Qt::UniqueConnection );
174175
connect( o2, &QgsO2::openBrowser, this, &QgsAuthOAuth2Method::onOpenBrowser, Qt::UniqueConnection );
175176
connect( o2, &QgsO2::closeBrowser, this, &QgsAuthOAuth2Method::onCloseBrowser, Qt::UniqueConnection );
176-
177+
connect( o2, &QgsO2::getAuthCode, this, &QgsAuthOAuth2Method::onAuthCode, Qt::UniqueConnection );
178+
connect( this, &QgsAuthOAuth2Method::setAuthCode, o2, &QgsO2::onSetAuthCode, Qt::UniqueConnection );
177179
//qRegisterMetaType<QNetworkReply::NetworkError>( QStringLiteral( "QNetworkReply::NetworkError" )) // for Qt::QueuedConnection, if needed;
178180
connect( o2, &QgsO2::refreshFinished, this, &QgsAuthOAuth2Method::onRefreshFinished, Qt::UniqueConnection );
179181

@@ -458,6 +460,16 @@ void QgsAuthOAuth2Method::onRefreshFinished( QNetworkReply::NetworkError err )
458460
}
459461
}
460462

463+
void QgsAuthOAuth2Method::onAuthCode()
464+
{
465+
bool ok = false;
466+
QString code = QInputDialog::getText( QApplication::activeWindow(), QStringLiteral( "Enter the authorization code" ) , QStringLiteral("Authoriation code" ), QLineEdit::Normal, QStringLiteral( "Required" ), &ok, Qt::Dialog, Qt::InputMethodHint::ImhNone);
467+
if( ok && !code.isEmpty())
468+
{
469+
emit setAuthCode(code);
470+
}
471+
}
472+
461473
bool QgsAuthOAuth2Method::updateDataSourceUriItems( QStringList &connectionItems, const QString &authcfg,
462474
const QString &dataprovider )
463475
{

‎src/auth/oauth2/qgsauthoauth2method.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,22 +71,36 @@ class QgsAuthOAuth2Method : public QgsAuthMethod
7171

7272
//! Triggered when linked condition has changed
7373
void onLinkedChanged();
74+
7475
//! Triggered when linking operation failed
7576
void onLinkingFailed();
77+
7678
//! Triggered when linking operation succeeded
7779
void onLinkingSucceeded();
7880

7981
//! Triggered when the browser needs to be opened at \a url
8082
void onOpenBrowser( const QUrl &url );
83+
8184
//! Triggered on browser close
8285
void onCloseBrowser();
86+
8387
//! Triggered on reply finished
8488
void onReplyFinished();
89+
8590
//! Triggered on network error
8691
void onNetworkError( QNetworkReply::NetworkError err );
92+
8793
//! Triggered on refresh finished
8894
void onRefreshFinished( QNetworkReply::NetworkError err );
8995

96+
//! Triggered when auth code needs to be manually entered by the user
97+
void onAuthCode();
98+
99+
signals:
100+
101+
//! Emitted when authcode was manually set by the user
102+
void setAuthCode( const QString code );
103+
90104
private:
91105
QString mTempStorePath;
92106

‎src/auth/oauth2/qgso2.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ void QgsO2::clearProperties()
144144
// TODO: clear object properties
145145
}
146146

147+
void QgsO2::onSetAuthCode(const QString& code)
148+
{
149+
setCode( code );
150+
onVerificationReceived( QMap<QString, QString>() );
151+
}
152+
147153
void QgsO2::link()
148154
{
149155
QgsDebugMsgLevel( QStringLiteral( "QgsO2::link" ), 4 );

‎src/auth/oauth2/qgso2.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ class QgsO2: public O2
6363
//! Clear all properties
6464
void clearProperties();
6565

66+
//! Triggered when auth code was set
67+
void onSetAuthCode(const QString &code);
68+
6669
//! Authenticate.
6770
void link() override;
6871

‎tests/src/auth/testqgsauthoauth2method.cpp

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "qgsapplication.h"
2929
#include "qgsauthmanager.h"
3030
#include "qgsauthoauth2config.h"
31+
#include "qgsauthoauth2edit.h"
3132

3233

3334
/**
@@ -47,6 +48,9 @@ class TestQgsAuthOAuth2Method: public QObject
4748
void testOAuth2Config();
4849
void testOAuth2ConfigIO();
4950
void testOAuth2ConfigUtils();
51+
void testDynamicRegistration();
52+
void testDynamicRegistrationJwt();
53+
void testDynamicRegistrationNoEndpoint();
5054

5155
private:
5256
QgsAuthOAuth2Config *baseConfig( bool loaded = false );
@@ -56,8 +60,13 @@ class TestQgsAuthOAuth2Method: public QObject
5660
QByteArray baseVariantTxt();
5761

5862
static QString smHashes;
63+
static QString sTestDataDir;
5964
};
6065

66+
67+
QString TestQgsAuthOAuth2Method::sTestDataDir = QStringLiteral( TEST_DATA_DIR ) + "/auth_system/oauth2";
68+
69+
6170
QString TestQgsAuthOAuth2Method::smHashes = "#####################";
6271
//QObject *TestQgsAuthOAuth2Method::smParentObj = new QObject();
6372

@@ -400,5 +409,100 @@ void TestQgsAuthOAuth2Method::testOAuth2ConfigUtils()
400409

401410
}
402411

412+
void TestQgsAuthOAuth2Method::testDynamicRegistrationNoEndpoint()
413+
{
414+
QgsAuthOAuth2Config *config = baseConfig();
415+
config->setClientId( QString( ));
416+
config->setClientSecret( QString( ));
417+
QVariantMap configMap( config->mappedProperties() );
418+
QCOMPARE(configMap["clientId"], QString());
419+
QCOMPARE(configMap["clientSecret"], QString());
420+
QgsAuthOAuth2Edit dlg;
421+
QgsStringMap stringMap;
422+
for ( const auto &k: configMap.keys( ))
423+
{
424+
stringMap[k] = configMap.value(k).toString();
425+
}
426+
dlg.loadConfig(stringMap);
427+
QCOMPARE(dlg.leClientId->text(), QString());
428+
QCOMPARE(dlg.leClientSecret->text(), QString());
429+
430+
// This JWT does not contain a registration_endpoint
431+
dlg.leSoftwareStatementJwtPath->setText( QStringLiteral( "%1/auth_code_grant_display_code.jwt" ).arg( sTestDataDir ) );
432+
QVERIFY( ! dlg.btnRegister->isEnabled() );
433+
QCOMPARE( dlg.leSoftwareStatementConfigUrl->text(), QString() );
434+
}
435+
436+
void TestQgsAuthOAuth2Method::testDynamicRegistration()
437+
{
438+
QgsAuthOAuth2Config *config = baseConfig();
439+
config->setClientId( QString( ));
440+
config->setClientSecret( QString( ));
441+
QVariantMap configMap( config->mappedProperties() );
442+
QCOMPARE(configMap["clientId"], QString());
443+
QCOMPARE(configMap["clientSecret"], QString());
444+
QgsAuthOAuth2Edit dlg;
445+
QgsStringMap stringMap;
446+
for ( const auto &k: configMap.keys( ))
447+
{
448+
stringMap[k] = configMap.value(k).toString();
449+
}
450+
dlg.loadConfig(stringMap);
451+
QCOMPARE(dlg.leClientId->text(), QString());
452+
QCOMPARE(dlg.leClientSecret->text(), QString());
453+
454+
// This JWT does not contain a registration_endpoint
455+
dlg.leSoftwareStatementJwtPath->setText( QStringLiteral( "%1/auth_code_grant_display_code.jwt" ).arg( sTestDataDir ) );
456+
QVERIFY( ! dlg.btnRegister->isEnabled() );
457+
QCOMPARE( dlg.leSoftwareStatementConfigUrl->text(), QString() );
458+
// Set the config url to something local
459+
dlg.leSoftwareStatementConfigUrl->setText( QUrl::fromLocalFile( QStringLiteral( "%1/auth_code_grant_display_code_get_config.json" ).arg( sTestDataDir )).toString( ));
460+
QVERIFY( dlg.btnRegister->isEnabled() );
461+
// Change it to something local
462+
dlg.setRegistrationEndpoint( QUrl::fromLocalFile( QStringLiteral( "%1/client_information_registration_response.json" ).arg( sTestDataDir )).toString());
463+
QTest::mouseClick( dlg.btnRegister, Qt::MouseButton::LeftButton );
464+
while ( dlg.mDownloading )
465+
{
466+
qApp->processEvents();
467+
}
468+
QCOMPARE(dlg.leClientId->text(), QLatin1Literal( "___QGIS_ROCKS___@www.qgis.org"));
469+
QCOMPARE(dlg.leClientSecret->text(), QLatin1Literal( "___QGIS_ROCKS______QGIS_ROCKS______QGIS_ROCKS___" ));
470+
}
471+
472+
473+
void TestQgsAuthOAuth2Method::testDynamicRegistrationJwt()
474+
{
475+
QgsAuthOAuth2Config *config = baseConfig();
476+
config->setClientId( QString( ));
477+
config->setClientSecret( QString( ));
478+
QVariantMap configMap( config->mappedProperties() );
479+
QCOMPARE(configMap["clientId"], QString());
480+
QCOMPARE(configMap["clientSecret"], QString());
481+
QgsAuthOAuth2Edit dlg;
482+
QgsStringMap stringMap;
483+
for ( const auto &k: configMap.keys( ))
484+
{
485+
stringMap[k] = configMap.value(k).toString();
486+
}
487+
dlg.loadConfig(stringMap);
488+
QCOMPARE(dlg.leClientId->text(), QString());
489+
QCOMPARE(dlg.leClientSecret->text(), QString());
490+
491+
// Now set the config URL to the JWT that does contain a registration_endpoint
492+
dlg.leSoftwareStatementJwtPath->setText( QStringLiteral( "%1/auth_code_grant_display_code_registration_endpoint.jwt" ).arg( sTestDataDir ));
493+
QCOMPARE( dlg.leSoftwareStatementConfigUrl->text(), QStringLiteral("http://www.qgis.org/oauth2/registration") );
494+
QVERIFY( dlg.btnRegister->isEnabled() );
495+
// Change it to something local
496+
dlg.setRegistrationEndpoint( QUrl::fromLocalFile( QStringLiteral( "%1/client_information_registration_response.json" ).arg( sTestDataDir )).toString() );
497+
QTest::mouseClick( dlg.btnRegister, Qt::MouseButton::LeftButton );
498+
while ( dlg.mDownloading )
499+
{
500+
qApp->processEvents();
501+
}
502+
QCOMPARE(dlg.leClientId->text(), QLatin1Literal( "___QGIS_ROCKS___@www.qgis.org"));
503+
QCOMPARE(dlg.leClientSecret->text(), QLatin1Literal( "___QGIS_ROCKS______QGIS_ROCKS______QGIS_ROCKS___" ));
504+
}
505+
506+
403507
QGSTEST_MAIN( TestQgsAuthOAuth2Method )
404508
#include "testqgsauthoauth2method.moc"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJ3d3cuc2VjdXJlLWRpbWVuc2lvbnMuZGUiLCJhdWQiOiJhcy50YjEzLnNlY3VyZS1kaW1lbnNpb25zLmRlIiwic29mdHdhcmVfaWQiOiIwZTNlZmYyMi01NjkwLTQzMDYtYWFmYi04ZDRkMGY1Yjk2N2IiLCJzb2Z0d2FyZV92ZXJzaW9uIjoiMi4xOC40eCIsImNsaWVudF9uYW1lIjoiUUdJUyBPQXV0aDIgUGx1Z2luIiwiY2xpZW50X3VyaSI6Imh0dHBzOi8vcWdpcy5vcmciLCJyZWRpcmVjdF91cmlzIjpbImh0dHBzOi8vYXMudGIxMy5zZWN1cmUtZGltZW5zaW9ucy5kZS9vYXV0aC9kaXNwbGF5X2NvZGUucGhwIl0sInRva2VuX2VuZHBvaW50X2F1dGhfbWV0aG9kIjoiY2xpZW50X3NlY3JldF9wb3N0IiwiZ3JhbnRfdHlwZXMiOlsiYXV0aG9yaXphdGlvbl9jb2RlIl0sInJlc3BvbnNlX3R5cGVzIjpbImNvZGUiXSwibG9nb191cmkiOiJodHRwczovL2h1Yi5xZ2lzLm9yZy9hdHRhY2htZW50cy80MDAzL1FHaXNfTG9nby5wbmciLCJzY29wZSI6Im9wZW5pZCBiZWVsZCIsImNvbnRhY3RzIjpbIm1haWx0bzphbUBzZWN1cmUtZGltZW5zaW9ucy5kZSJdLCJ0b3NfdXJpIjoiaHR0cHM6Ly9hcy50YjEzLnNlY3VyZS1kaW1lbnNpb25zLmRlL3FnaXMtdG9zLmh0bWwiLCJwb2xpY3lfdXJpIjoiaHR0cHM6Ly9hcy50YjEzLnNlY3VyZS1kaW1lbnNpb25zLmRlL3FnaXMtcG9saWN5Lmh0bWwiLCJqd2tzX3VyaSI6Imh0dHBzOi8vYXMudGIxMy5zZWN1cmUtZGltZW5zaW9ucy5kZS8ud2VsbC1rbm93bi9qd2tzLmpzb24iLCJraWQiOiJTRFB1YmxpY0tleSIsImlhdCI6MTQ5OTE1MDY3M30.M4ablN_BfmHjVPYZo_uVrCDYLsaalhF_aRsS-hppT9AJd8DAGZn_xYrsq8FIaKjZhivjKHtjbR8zyFee0HjU6iMP9KgC0N_jPn3o5L2n8IEl7oRJ5zW9V-v2SqBdaXffm7TBx7v8KQ2J4uaoARYWgAkBcVVxKZWX9kLgQfaaoSu2Zk-aoMNqzNU8u2UTADpoCwGjOd10ik0ZdQ6VC2czfAfYEmfYg4UYPQDafKjWWEXUhO9PXzD0piv_NJ9o2oDdc34KBj8brZRDbDfuvIP5dxpaDh8PWhVE4Dd7dEsAzPXEFdXnLTGzQ_5zDj_CcUQqJ0SzkohALGhpPgy51Bjwsw
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"registration_endpoint": "http://www.qgis.org/oauth2/register"
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjbGllbnRfbmFtZSI6IlFHSVMgT0F1dGgyIFBsdWdpbiIsInNvZnR3YXJlX3ZlcnNpb24iOiIyLjE4LjR4IiwicG9saWN5X3VyaSI6Imh0dHBzOi8vYXMudGIxMy5zZWN1cmUtZGltZW5zaW9ucy5kZS9xZ2lzLXBvbGljeS5odG1sIiwic29mdHdhcmVfaWQiOiIwZTNlZmYyMi01NjkwLTQzMDYtYWFmYi04ZDRkMGY1Yjk2N2IiLCJjbGllbnRfdXJpIjoiaHR0cHM6Ly9xZ2lzLm9yZyIsImdyYW50X3R5cGVzIjpbImF1dGhvcml6YXRpb25fY29kZSJdLCJpc3MiOiJ3d3cuc2VjdXJlLWRpbWVuc2lvbnMuZGUiLCJyZXNwb25zZV90eXBlcyI6WyJjb2RlIl0sImF1ZCI6ImFzLnRiMTMuc2VjdXJlLWRpbWVuc2lvbnMuZGUiLCJ0b3NfdXJpIjoiaHR0cHM6Ly9hcy50YjEzLnNlY3VyZS1kaW1lbnNpb25zLmRlL3FnaXMtdG9zLmh0bWwiLCJ0b2tlbl9lbmRwb2ludF9hdXRoX21ldGhvZCI6ImNsaWVudF9zZWNyZXRfcG9zdCIsImNvbnRhY3RzIjpbIm1haWx0bzphbUBzZWN1cmUtZGltZW5zaW9ucy5kZSJdLCJqd2tzX3VyaSI6Imh0dHBzOi8vYXMudGIxMy5zZWN1cmUtZGltZW5zaW9ucy5kZS8ud2VsbC1rbm93bi9qd2tzLmpzb24iLCJzY29wZSI6Im9wZW5pZCBiZWVsZCIsInJlZGlyZWN0X3VyaXMiOlsiaHR0cHM6Ly9hcy50YjEzLnNlY3VyZS1kaW1lbnNpb25zLmRlL29hdXRoL2Rpc3BsYXlfY29kZS5waHAiXSwicmVnaXN0cmF0aW9uX2VuZHBvaW50IjoiaHR0cDovL3d3dy5xZ2lzLm9yZy9vYXV0aDIvcmVnaXN0cmF0aW9uIiwibG9nb191cmkiOiJodHRwczovL2h1Yi5xZ2lzLm9yZy9hdHRhY2htZW50cy80MDAzL1FHaXNfTG9nby5wbmciLCJraWQiOiJTRFB1YmxpY0tleSJ9.kd5lIGXjCYPIgffKton3NaTSdIB8KueYpu48vXbydGY
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"client_id": "___QGIS_ROCKS___@www.qgis.org",
3+
"client_secret": "___QGIS_ROCKS______QGIS_ROCKS______QGIS_ROCKS___"
4+
}

0 commit comments

Comments
 (0)
Please sign in to comment.