Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[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.
  • Loading branch information
elpaso committed Jul 19, 2018
1 parent c50e99e commit d56fc88
Show file tree
Hide file tree
Showing 12 changed files with 510 additions and 7 deletions.
242 changes: 239 additions & 3 deletions src/auth/oauth2/qgsauthoauth2edit.cpp
Expand Up @@ -22,8 +22,9 @@
#include "qgsauthguiutils.h"
#include "qgsauthmanager.h"
#include "qgsauthconfigedit.h"
#include "qgslogger.h"

#include "qgsmessagelog.h"
#include "qgsnetworkaccessmanager.h"
#include "qjsonwrapper/Json.h"

QgsAuthOAuth2Edit::QgsAuthOAuth2Edit( QWidget *parent )
: QgsAuthMethodEdit( parent )
Expand Down Expand Up @@ -146,6 +147,17 @@ void QgsAuthOAuth2Edit::setupConnections()
connect( btnGetDefinedDirPath, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::getDefinedCustomDir );
connect( leDefinedDirPath, &QLineEdit::textChanged, this, &QgsAuthOAuth2Edit::definedCustomDirChanged );

connect( btnSoftStatementDir, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::getSoftStatementDir );
connect( leSoftwareStatementJwtPath, &QLineEdit::textChanged,this, &QgsAuthOAuth2Edit::softwareStatementJwtPathChanged );
connect( leSoftwareStatementConfigUrl, &QLineEdit::textChanged, [ = ] ( const QString &txt ) {
btnRegister->setEnabled( QUrl( txt ).isValid() && ! leSoftwareStatementJwtPath->text().isEmpty() );
});
connect( btnRegister, &QPushButton::clicked, this, &QgsAuthOAuth2Edit::getSoftwareStatementConfig );

// FIXME: in the testbed13 code this signal does not exists (but a connection was attempted)
//connect( this, &QgsAuthOAuth2Edit::configSucceeded, this, &QgsAuthOAuth2Edit::registerSoftStatement );


// Custom config editing connections
connect( cmbbxGrantFlow, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
this, &QgsAuthOAuth2Edit::updateGrantFlow ); // also updates GUI
Expand Down Expand Up @@ -353,7 +365,6 @@ void QgsAuthOAuth2Edit::clearConfig()
loadFromOAuthConfig( mOAuthConfigCustom.get() );
}

// slot
void QgsAuthOAuth2Edit::loadFromOAuthConfig( const QgsAuthOAuth2Config *config )
{
if ( !config )
Expand Down Expand Up @@ -494,6 +505,21 @@ void QgsAuthOAuth2Edit::definedCustomDirChanged( const QString &path )
}
}


void QgsAuthOAuth2Edit::softwareStatementJwtPathChanged( const QString &path )
{
QFileInfo pinfo( path );
bool ok = pinfo.exists() || pinfo.isFile();

leSoftwareStatementJwtPath->setStyleSheet( ok ? "" : QgsAuthGuiUtils::redTextStyleSheet() );

if ( ok )
{
parseSoftwareStatement( path );
}
}


// slot
void QgsAuthOAuth2Edit::setCurrentDefinedConfig( const QString &id )
{
Expand Down Expand Up @@ -557,6 +583,20 @@ void QgsAuthOAuth2Edit::getDefinedCustomDir()
leDefinedDirPath->setText( extradir );
}

void QgsAuthOAuth2Edit::getSoftStatementDir()
{
QString softStatementFile = QFileDialog::getOpenFileName( this, tr( "Select software statement file" ),
QDir::homePath(), tr( "JSON Web Token (*.jwt)") );
this->raise();
this->activateWindow();

if ( softStatementFile.isNull() )
{
return;
}
leSoftwareStatementJwtPath->setText( softStatementFile );
}

void QgsAuthOAuth2Edit::initConfigObjs()
{
mOAuthConfigCustom = qgis::make_unique<QgsAuthOAuth2Config>( nullptr );
Expand Down Expand Up @@ -673,6 +713,11 @@ bool QgsAuthOAuth2Edit::onDefinedTab() const
return mCurTab == definedTab();
}

bool QgsAuthOAuth2Edit::onStatementTab() const
{
return mCurTab == statementTab();
}

// slot
void QgsAuthOAuth2Edit::updateGrantFlow( int indx )
{
Expand Down Expand Up @@ -910,3 +955,194 @@ void QgsAuthOAuth2Edit::clearQueryPairs()
tblwdgQueryPairs->removeRow( i - 1 );
}
}

void QgsAuthOAuth2Edit::parseSoftwareStatement(const QString& path)
{
QFile file(path);
QByteArray softwareStatementBase64;
if(file.open(QIODevice::ReadOnly | QIODevice::Text))
{
softwareStatementBase64=file.readAll();
}
if(softwareStatementBase64.isEmpty())
{
QgsDebugMsg( QStringLiteral( "Error software statement is empty: %1" ).arg( QString( path ) ) );
file.close();
return;
}
file.close();
mSoftwareStatement.insert("software_statement",softwareStatementBase64);
QByteArray payload=softwareStatementBase64.split('.')[1];
QByteArray decoded=QByteArray::fromBase64(payload/*, QByteArray::Base64UrlEncoding*/);
QByteArray errStr;
bool res = false;
QMap<QString, QVariant> jsonData = QJsonWrapper::parseJson(decoded, &res, &errStr).toMap();
if ( !res )
{
QgsDebugMsg( QStringLiteral( "Error parsing JSON: %1" ).arg( QString( errStr ) ));
return;
}
if(jsonData.contains("grant_types") && jsonData.contains( QLatin1Literal( "redirect_uris" ) ) )
{
QString grantType = jsonData[QLatin1Literal ( "grant_types" ) ].toStringList()[0];
if(grantType == QLatin1Literal( "authorization_code" ) )
{
updateGrantFlow( static_cast<int>( QgsAuthOAuth2Config::AuthCode ) );
}
else
{
updateGrantFlow( static_cast<int>( QgsAuthOAuth2Config::ResourceOwner ) );
}
//Set redirect_uri
QString redirectUri = jsonData[QLatin1Literal( "redirect_uris" ) ].toStringList()[0];
leRedirectUrl->setText(redirectUri);
}
else
{
QgsDebugMsgLevel( QStringLiteral( "Error software statement is invalid: %1" ).arg( QString( path ) ), 4 );
return;
}
if(jsonData.contains(QLatin1Literal( "registration_endpoint")) )
{
mRegistrationEndpoint = jsonData[QLatin1Literal("registration_endpoint")].toString();
leSoftwareStatementConfigUrl->setText( mRegistrationEndpoint );
}
QgsDebugMsgLevel( QStringLiteral( "JSON: %1" ).arg( QString::fromLocal8Bit( decoded.data() ) ), 4 );
}

void QgsAuthOAuth2Edit::configReplyFinished()
{
qDebug() << "QgsAuthOAuth2Edit::onConfigReplyFinished";
QNetworkReply *configReply = qobject_cast<QNetworkReply *>(sender());
if (configReply->error() == QNetworkReply::NoError)
{
QByteArray replyData = configReply->readAll();
QByteArray errStr;
bool res = false;
QVariantMap config = QJsonWrapper::parseJson(replyData, &res, &errStr).toMap();

if ( !res )
{
QgsDebugMsg( QStringLiteral( "Error parsing JSON: %1" ).arg( QString( errStr ) ) );
return;
}
// I haven't found any docs about the content of this confg JSON file
// I assume that registration_endpoint is all that it contains
// But we also might have other optional information here
if(config.contains(QLatin1Literal( "registration_endpoint")) )
{
if ( config.contains(QLatin1Literal("authorization_endpoint" ) ) )
leRequestUrl->setText(config.value(QLatin1Literal("authorization_endpoint" ) ).toString());
if ( config.contains(QLatin1Literal("token_endpoint" ) ) )
leTokenUrl->setText(config.value(QLatin1Literal("token_endpoint" ) ).toString());

registerSoftStatement(config.value(QLatin1Literal("registration_endpoint")).toString());
}
else
{
QString errorMsg = QStringLiteral( "Downloading configuration failed with error: %1" ).arg( configReply->errorString() );
QgsMessageLog::logMessage( errorMsg, QStringLiteral( "OAuth2" ), Qgis::Critical );
}
}
mDownloading = false;
configReply->deleteLater();
}

void QgsAuthOAuth2Edit::registerReplyFinished()
{
//JSV todo
//better error handling
qDebug() << "QgsAuthOAuth2Edit::onRegisterReplyFinished";
QNetworkReply *registerReply = qobject_cast<QNetworkReply *>(sender());
if (registerReply->error() == QNetworkReply::NoError)
{
QByteArray replyData = registerReply->readAll();
QByteArray errStr;
bool res = false;
QVariantMap clientInfo = QJsonWrapper::parseJson(replyData, &res, &errStr).toMap();

// According to RFC 7591 sec. 3.2.1. Client Information Response the only
// required field is client_id
leClientId->setText(clientInfo.value(QLatin1Literal("client_id" ) ).toString());
if ( clientInfo.contains(QLatin1Literal("client_secret" )) )
leClientSecret->setText(clientInfo.value(QLatin1Literal("client_secret" ) ).toString());
if ( clientInfo.contains(QLatin1Literal("authorization_endpoint" ) ) )
leRequestUrl->setText(clientInfo.value(QLatin1Literal("authorization_endpoint" ) ).toString());
if ( clientInfo.contains(QLatin1Literal("token_endpoint" ) ) )
leTokenUrl->setText(clientInfo.value(QLatin1Literal("token_endpoint" ) ).toString());
if ( clientInfo.contains(QLatin1Literal("scopes" ) ) )
leScope->setText(clientInfo.value(QLatin1Literal("scopes" ) ).toString());

tabConfigs->setCurrentIndex(0);
}
else
{
QString errorMsg = QStringLiteral( "Client registration failed with error: %1" ).arg( registerReply->errorString() );
QgsMessageLog::logMessage( errorMsg, QLatin1Literal( "OAuth2" ) , Qgis::Critical);
}
mDownloading = false;
registerReply->deleteLater();
}

void QgsAuthOAuth2Edit::networkError(QNetworkReply::NetworkError error)
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
qWarning() << "QgsAuthOAuth2Edit::onNetworkError: " << error << ": " << reply->errorString();
QString errorMsg = QStringLiteral( "Network error: %1" ).arg( reply->errorString() );
QgsMessageLog::logMessage( errorMsg, QLatin1Literal( "OAuth2" ), Qgis::Critical );
qDebug() << "QgsAuthOAuth2Edit::onNetworkError: " << reply->readAll();
}


void QgsAuthOAuth2Edit::registerSoftStatement(const QString& registrationUrl)
{
QUrl regUrl(registrationUrl);
if( !regUrl.isValid() )
{
qWarning()<<"Registration url is not valid";
return;
}
QByteArray errStr;
bool res = false;
QByteArray json = QJsonWrapper::toJson(QVariant(mSoftwareStatement),&res,&errStr);
QNetworkRequest registerRequest(regUrl);
registerRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1Literal( "application/json") );
QNetworkReply * registerReply;
// For testability: use GET if protocol is file://
if ( regUrl.scheme() == QLatin1Literal( "file" ) )
registerReply = QgsNetworkAccessManager::instance()->get(registerRequest);
else
registerReply = QgsNetworkAccessManager::instance()->post(registerRequest, json);
mDownloading = true;
connect(registerReply, &QNetworkReply::finished, this, &QgsAuthOAuth2Edit::registerReplyFinished, Qt::QueuedConnection);
connect(registerReply, qgis::overload<QNetworkReply::NetworkError>::of( &QNetworkReply::error ), this, &QgsAuthOAuth2Edit::networkError, Qt::QueuedConnection);
}

void QgsAuthOAuth2Edit::getSoftwareStatementConfig()
{
if(!mRegistrationEndpoint.isEmpty())
{
registerSoftStatement(mRegistrationEndpoint);
}
else
{
QString config = leSoftwareStatementConfigUrl->text();
QUrl configUrl(config);
QNetworkRequest configRequest(configUrl);
QNetworkReply * configReply = QgsNetworkAccessManager::instance()->get(configRequest);
mDownloading = true;
connect(configReply, &QNetworkReply::finished, this, &QgsAuthOAuth2Edit::configReplyFinished, Qt::QueuedConnection);
connect(configReply, qgis::overload<QNetworkReply::NetworkError>::of( &QNetworkReply::error ), this, &QgsAuthOAuth2Edit::networkError, Qt::QueuedConnection);
}
}

QString QgsAuthOAuth2Edit::registrationEndpoint() const
{
return mRegistrationEndpoint;
}

void QgsAuthOAuth2Edit::setRegistrationEndpoint(const QString& registrationEndpoint)
{
mRegistrationEndpoint = registrationEndpoint;
}

42 changes: 41 additions & 1 deletion src/auth/oauth2/qgsauthoauth2edit.h
Expand Up @@ -16,6 +16,7 @@
#define QGSAUTHOAUTH2EDIT_H

#include <QWidget>
#include <QNetworkReply>
#include "qgsauthmethodedit.h"
#include "ui_qgsauthoauth2edit.h"

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


public slots:

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

void populateGrantFlows();

void updateGrantFlow( int indx );

void exportOAuthConfig();

void importOAuthConfig();

void descriptionChanged();

void populateAccessMethods();

void updateConfigAccessMethod( int indx );

void addQueryPair();

void removeQueryPair();

void clearQueryPairs();

void populateQueryPairs( const QVariantMap &querypairs, bool append = false );

void queryTableSelectionChanged();

void updateConfigQueryPairs();

void updateDefinedConfigsCache();

void loadDefinedConfigs();

void setCurrentDefinedConfig( const QString &id );

void currentDefinedItemChanged( QListWidgetItem *cur, QListWidgetItem *prev );

void selectCurrentDefinedConfig();

void loadFromOAuthConfig( const QgsAuthOAuth2Config *config = nullptr );
void getSoftStatementDir();

void updateTokenCacheFile( bool curpersist ) const;

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

void getDefinedCustomDir();

void loadFromOAuthConfig( const QgsAuthOAuth2Config *config );

void softwareStatementJwtPathChanged( const QString &path );

void configReplyFinished();

void registerReplyFinished();

void networkError(QNetworkReply::NetworkError error);

//! For testability
QString registrationEndpoint() const;

//! For testability
void setRegistrationEndpoint(const QString& registrationEndpoint);

private:

void initGui();
void parseSoftwareStatement(const QString& path);

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

int customTab() const { return 0; }
int definedTab() const { return 1; }
int statementTab() const { return 2; }
bool onCustomTab() const;
bool onDefinedTab() const;
bool onStatementTab() const;
void getSoftwareStatementConfig();

QString currentDefinedConfig() const { return mDefinedId; }

Expand All @@ -132,6 +167,11 @@ class QgsAuthOAuth2Edit : public QgsAuthMethodEdit, private Ui::QgsAuthOAuth2Edi
int mCurTab = 0;
bool mPrevPersistToken = false;
QToolButton *btnTokenClear = nullptr;
QString mRegistrationEndpoint;
QMap<QString, QVariant> mSoftwareStatement;
void registerSoftStatement(const QString& registrationUrl);
bool mDownloading = false;
friend class TestQgsAuthOAuth2Method;
};

#endif // QGSAUTHOAUTH2EDIT_H

0 comments on commit d56fc88

Please sign in to comment.