Skip to content
Permalink
Browse files

[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 d56fc885f6469982ea84bc9e177d649b0dc1f384
@@ -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 )
@@ -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
@@ -353,7 +365,6 @@ void QgsAuthOAuth2Edit::clearConfig()
loadFromOAuthConfig( mOAuthConfigCustom.get() );
}

// slot
void QgsAuthOAuth2Edit::loadFromOAuthConfig( const QgsAuthOAuth2Config *config )
{
if ( !config )
@@ -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 )
{
@@ -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 );
@@ -673,6 +713,11 @@ bool QgsAuthOAuth2Edit::onDefinedTab() const
return mCurTab == definedTab();
}

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

// slot
void QgsAuthOAuth2Edit::updateGrantFlow( int indx )
{
@@ -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;
}

@@ -16,6 +16,7 @@
#define QGSAUTHOAUTH2EDIT_H

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

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


public slots:

//! Load the configuration from \a configMap
@@ -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;

@@ -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;
@@ -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; }

@@ -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.
You can’t perform that action at this time.