Skip to content

Commit c178f4a

Browse files
authored
Merge pull request #6111 from opengisch/sso_client
Add foundation to SSO support to log into QFieldCloud
2 parents 088700c + f26f374 commit c178f4a

14 files changed

+409
-45
lines changed

platform/android/AndroidManifest.xml.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
android:label="@APP_NAME@"
4949
android:icon="@AT@drawable/@APP_ICON@"
5050
android:usesCleartextTraffic="true"
51+
android:networkSecurityConfig="@xml/network_security_config"
5152
android:requestLegacyExternalStorage="true"
5253
android:theme="@style/AppTheme">
5354
<activity
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<network-security-config>
2+
<base-config>
3+
<trust-anchors>
4+
<certificates src="user"/>
5+
<certificates src="system"/>
6+
</trust-anchors>
7+
</base-config>
8+
</network-security-config>

resources/resources.qrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@
1212
<qresource prefix="/sounds">
1313
<file alias="proximity_alarm.wav">sounds/proximity_alarm.wav</file>
1414
</qresource>
15+
<qresource prefix="/oauth2method">
16+
<file alias="oauth2_verification_finished.html">templates/oauth2_verification_finished.html</file>
17+
</qresource>
1518
</RCC>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script>
5+
// push any fragment as a query string to redirect server
6+
window.onload = function hashFunction() {
7+
var query = location.hash.substr(1);
8+
if (query != "") {
9+
var xhttp = new XMLHttpRequest();
10+
xhttp.open("GET", "/?" + query, true);
11+
xhttp.send();
12+
}
13+
};
14+
</script>
15+
<style>
16+
body {
17+
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
18+
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
19+
}
20+
</style>
21+
</head>
22+
<body>
23+
<!-- Replacement of below placeholders is done by src/auth/oauth2/core/qgso2.cpp -->
24+
25+
<h2>QField OAuth2 verification has finished</h2>
26+
27+
<p>If you have not been returned to QField, bring the application to the forefront.</p>
28+
29+
<p><a href="#" onclick="window.close()">Close window</a></p>
30+
</body>
31+
</html>

src/core/qfieldappauthrequesthandler.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ void QFieldAppAuthRequestHandler::enterCredentials( const QString &realm, const
3939
QgsCredentials::instance()->put( realm, username, password );
4040
}
4141

42+
bool QFieldAppAuthRequestHandler::isProjectLoading() const
43+
{
44+
return mIsProjectLoading;
45+
}
46+
47+
void QFieldAppAuthRequestHandler::setIsProjectLoading( bool loading )
48+
{
49+
if ( mIsProjectLoading == loading )
50+
return;
51+
52+
mIsProjectLoading = loading;
53+
emit isProjectLoadingChanged();
54+
}
55+
4256
bool QFieldAppAuthRequestHandler::hasPendingAuthRequest() const
4357
{
4458
if ( mBrowserAuthenticationOngoing )

src/core/qfieldappauthrequesthandler.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class QFieldAppAuthRequestHandler : public QObject, public QgsCredentials, publi
3838
{
3939
Q_OBJECT
4040

41+
Q_PROPERTY( bool isProjectLoading READ isProjectLoading WRITE setIsProjectLoading NOTIFY isProjectLoadingChanged )
4142
Q_PROPERTY( bool hasPendingAuthRequest READ hasPendingAuthRequest NOTIFY hasPendingAuthRequestChanged )
4243

4344
public:
@@ -60,6 +61,12 @@ class QFieldAppAuthRequestHandler : public QObject, public QgsCredentials, publi
6061
//! abort an ongoing external browser authentication request
6162
Q_INVOKABLE void abortAuthBrowser();
6263

64+
//! returns TRUE is a project is loading
65+
bool isProjectLoading() const;
66+
67+
//! sets whether a project is \a loading
68+
void setIsProjectLoading( bool loading );
69+
6370
//! returns the number of pending authentication requests
6471
bool hasPendingAuthRequest() const;
6572

@@ -69,6 +76,7 @@ class QFieldAppAuthRequestHandler : public QObject, public QgsCredentials, publi
6976
void reloadEverything();
7077
void showLoginBrowser( const QString &url );
7178
void hideLoginBrowser();
79+
void isProjectLoadingChanged();
7280
void hasPendingAuthRequestChanged();
7381

7482
protected:
@@ -102,6 +110,8 @@ class QFieldAppAuthRequestHandler : public QObject, public QgsCredentials, publi
102110

103111
QList<RealmEntry> mRealms;
104112
bool mBrowserAuthenticationOngoing = false;
113+
114+
bool mIsProjectLoading = false;
105115
};
106116

107117
#endif // QFIELDAPPAUTHREQUESTHANDLER_H

src/core/qfieldcloudconnection.cpp

Lines changed: 173 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <QTimer>
3131
#include <QUrlQuery>
3232
#include <qgsapplication.h>
33+
#include <qgsauthmanager.h>
3334
#include <qgsmessagelog.h>
3435
#include <qgsnetworkaccessmanager.h>
3536
#include <qgssettings.h>
@@ -39,12 +40,20 @@ QFieldCloudConnection::QFieldCloudConnection()
3940
: mUrl( QSettings().value( QStringLiteral( "/QFieldCloud/url" ), defaultUrl() ).toString() )
4041
, mUsername( QSettings().value( QStringLiteral( "/QFieldCloud/username" ) ).toString() )
4142
, mToken( QSettings().value( QStringLiteral( "/QFieldCloud/token" ) ).toByteArray() )
43+
, mProvider( QSettings().value( QStringLiteral( "/QFieldCloud/provider" ) ).toString() )
44+
, mProviderConfigId( QSettings().value( QStringLiteral( "/QFieldCloud/providerConfigId" ) ).toString() )
4245
{
4346
QgsNetworkAccessManager::instance()->setTimeout( 60 * 60 * 1000 );
4447
QgsNetworkAccessManager::instance()->setTransferTimeout( 5 * 60 * 1000 );
4548
// we cannot use "/" as separator, since QGIS puts a suffix QGIS/31700 anyway
4649
const QString userAgent = QStringLiteral( "qfield|%1|%2|%3|" ).arg( qfield::appVersion, qfield::appVersionStr.normalized( QString::NormalizationForm_KD ), qfield::gitRev );
4750
QgsSettings().setValue( QStringLiteral( "/qgis/networkAndProxy/userAgent" ), userAgent );
51+
52+
if ( !QgsApplication::authManager()->availableAuthMethodConfigs().contains( mProviderConfigId ) )
53+
{
54+
mProviderConfigId.clear();
55+
QSettings().remove( "/QFieldCloud/providerConfigId" );
56+
}
4857
}
4958

5059
QMap<QString, QString> QFieldCloudConnection::sErrors = QMap<QString, QString>(
@@ -104,14 +113,30 @@ QStringList QFieldCloudConnection::urls() const
104113
return savedUrls;
105114
}
106115

107-
QString QFieldCloudConnection::username() const
116+
QString QFieldCloudConnection::avatarUrl() const
108117
{
109-
return mUsername;
118+
return mAvatarUrl;
110119
}
111120

112-
QString QFieldCloudConnection::avatarUrl() const
121+
QString QFieldCloudConnection::provider() const
113122
{
114-
return mAvatarUrl;
123+
return mProvider;
124+
}
125+
126+
void QFieldCloudConnection::setProvider( const QString &provider )
127+
{
128+
if ( mProvider == provider )
129+
return;
130+
131+
mProvider = provider;
132+
QSettings().setValue( QStringLiteral( "/QFieldCloud/provider" ), provider );
133+
134+
emit providerChanged();
135+
}
136+
137+
QString QFieldCloudConnection::username() const
138+
{
139+
return mUsername;
115140
}
116141

117142
void QFieldCloudConnection::setUsername( const QString &username )
@@ -149,9 +174,74 @@ CloudUserInformation QFieldCloudConnection::userInformation() const
149174
return mUserInformation;
150175
}
151176

177+
bool QFieldCloudConnection::isFetchingAvailableProviders() const
178+
{
179+
return mIsFetchingAvailableProviders;
180+
}
181+
182+
QList<AuthenticationProvider> QFieldCloudConnection::availableProviders() const
183+
{
184+
return mAvailableProviders.values();
185+
}
186+
187+
void QFieldCloudConnection::getAuthenticationProviders()
188+
{
189+
if ( !mAvailableProviders.isEmpty() )
190+
{
191+
mAvailableProviders.clear();
192+
emit availableProvidersChanged();
193+
}
194+
195+
mIsFetchingAvailableProviders = true;
196+
emit isFetchingAvailableProvidersChanged();
197+
198+
QNetworkRequest request;
199+
request.setHeader( QNetworkRequest::ContentTypeHeader, "application/json" );
200+
request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy );
201+
NetworkReply *reply = get( request, "/api/v1/auth/providers/" );
202+
203+
connect( reply, &NetworkReply::finished, this, [=]() {
204+
QNetworkReply *rawReply = reply->currentRawReply();
205+
206+
Q_ASSERT( reply->isFinished() );
207+
Q_ASSERT( rawReply );
208+
209+
reply->deleteLater();
210+
rawReply->deleteLater();
211+
212+
mIsFetchingAvailableProviders = false;
213+
emit isFetchingAvailableProvidersChanged();
214+
215+
if ( rawReply->error() != QNetworkReply::NoError )
216+
{
217+
return;
218+
}
219+
220+
const QVariantList providers = QJsonDocument::fromJson( rawReply->readAll() ).toVariant().toList();
221+
for ( const QVariant &provider : providers )
222+
{
223+
const QVariantMap providerDetails = provider.toMap();
224+
const QString providerId = providerDetails.value( QStringLiteral( "id" ) ).toString();
225+
mAvailableProviders[providerId] = AuthenticationProvider( providerId, providerDetails.value( QStringLiteral( "name" ) ).toString(), providerDetails );
226+
}
227+
emit availableProvidersChanged();
228+
} );
229+
}
230+
152231
void QFieldCloudConnection::login()
153232
{
154-
const bool loginUsingToken = !mToken.isEmpty() && ( mPassword.isEmpty() || mUsername.isEmpty() );
233+
if ( !mProvider.isEmpty() )
234+
{
235+
if ( mProviderConfigId.isEmpty() && !mAvailableProviders.contains( mProvider ) )
236+
{
237+
emit loginFailed( tr( "Authentication provider missing" ) );
238+
return;
239+
}
240+
}
241+
242+
setStatus( ConnectionStatus::Connecting );
243+
244+
const bool loginUsingToken = !mProvider.isEmpty() || ( !mToken.isEmpty() && ( mPassword.isEmpty() || mUsername.isEmpty() ) );
155245
NetworkReply *reply = loginUsingToken
156246
? get( QStringLiteral( "/api/v1/auth/user/" ) )
157247
: post( QStringLiteral( "/api/v1/auth/token/" ), QVariantMap(
@@ -160,8 +250,6 @@ void QFieldCloudConnection::login()
160250
{ "password", mPassword },
161251
} ) );
162252

163-
setStatus( ConnectionStatus::Connecting );
164-
165253
// Handle login redirect as an error state
166254
connect( reply, &NetworkReply::redirected, this, [=]() {
167255
QNetworkReply *rawReply = reply->currentRawReply();
@@ -212,6 +300,14 @@ void QFieldCloudConnection::login()
212300
emit loginFailed( message );
213301
}
214302

303+
if ( !mProvider.isEmpty() && !mProviderConfigId.isEmpty() )
304+
{
305+
QgsApplication::instance()->authManager()->removeAuthenticationConfig( mProviderConfigId );
306+
mProviderConfigId.clear();
307+
QSettings().remove( "/QFieldCloud/providerConfigId" );
308+
emit providerConfigurationChanged();
309+
}
310+
215311
setStatus( ConnectionStatus::Disconnected );
216312
return;
217313
}
@@ -258,7 +354,7 @@ void QFieldCloudConnection::logout()
258354
QgsNetworkAccessManager *nam = QgsNetworkAccessManager::instance();
259355
QNetworkRequest request( mUrl + QStringLiteral( "/api/v1/auth/logout/" ) );
260356
request.setHeader( QNetworkRequest::ContentTypeHeader, "application/json" );
261-
setAuthenticationToken( request );
357+
setAuthenticationDetails( request );
262358

263359
QNetworkReply *reply = nam->post( request, QByteArray() );
264360

@@ -272,6 +368,14 @@ void QFieldCloudConnection::logout()
272368
mAvatarUrl.clear();
273369
emit avatarUrlChanged();
274370

371+
if ( !mProviderConfigId.isEmpty() )
372+
{
373+
QgsApplication::instance()->authManager()->removeAuthenticationConfig( mProviderConfigId );
374+
mProviderConfigId.clear();
375+
QSettings().remove( "/QFieldCloud/providerConfigId" );
376+
emit providerConfigurationChanged();
377+
}
378+
275379
setStatus( ConnectionStatus::Disconnected );
276380
}
277381

@@ -289,7 +393,7 @@ NetworkReply *QFieldCloudConnection::post( const QString &endpoint, const QVaria
289393
{
290394
QNetworkRequest request( mUrl + endpoint );
291395
QByteArray requestBody = QJsonDocument( QJsonObject::fromVariantMap( params ) ).toJson();
292-
setAuthenticationToken( request );
396+
setAuthenticationDetails( request );
293397
request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy );
294398

295399
if ( fileNames.isEmpty() )
@@ -360,7 +464,7 @@ NetworkReply *QFieldCloudConnection::get( const QString &endpoint, const QVarian
360464

361465
request.setHeader( QNetworkRequest::ContentTypeHeader, "application/json" );
362466
request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy );
363-
setAuthenticationToken( request );
467+
setAuthenticationDetails( request );
364468

365469
return get( request, endpoint, params );
366470
}
@@ -463,12 +567,70 @@ void QFieldCloudConnection::setState( ConnectionState state )
463567
emit stateChanged();
464568
}
465569

466-
void QFieldCloudConnection::setAuthenticationToken( QNetworkRequest &request )
570+
void QFieldCloudConnection::setAuthenticationDetails( QNetworkRequest &request )
467571
{
468572
if ( !mToken.isNull() )
469573
{
470574
request.setRawHeader( "Authorization", "Token " + mToken );
471575
}
576+
577+
if ( !mProvider.isEmpty() )
578+
{
579+
QString providerId;
580+
if ( mProviderConfigId.isEmpty() && mAvailableProviders.contains( mProvider ) )
581+
{
582+
const QVariantMap providerDetails = mAvailableProviders[mProvider].details();
583+
providerId = providerDetails.value( "id" ).toString();
584+
585+
QVariantMap configMap;
586+
configMap["accessMethod"] = 0;
587+
configMap["clientId"] = providerDetails.value( "client_id" ).toString();
588+
configMap["clientSecret"] = providerDetails.value( "client_secret" ).toString();
589+
configMap["configType"] = 1;
590+
configMap["description"] = QString( "Connection details for QFieldCloud using %1 provider" ).arg( mProvider );
591+
configMap["extraTokens"] = providerDetails.value( "extra_tokens" ).toMap();
592+
configMap["grantFlow"] = providerDetails.value( "grant_flow" ).toInt();
593+
configMap["name"] = QString( "Autogenerated by QField" );
594+
configMap["persistToken"] = true;
595+
configMap["redirectHost"] = QString( "localhost" );
596+
configMap["redirectPort"] = 7070;
597+
configMap["refreshTokenUrl"] = providerDetails.value( "refresh_token_url" ).toString();
598+
configMap["requestTimeout"] = 30;
599+
configMap["requestUrl"] = providerDetails.value( "request_url" ).toString();
600+
configMap["scope"] = providerDetails.value( "scope" ).toString();
601+
configMap["tokenUrl"] = providerDetails.value( "token_url" ).toString();
602+
configMap["version"] = 1;
603+
QJsonDocument json = QJsonDocument::fromVariant( configMap );
604+
605+
QgsAuthMethodConfig config;
606+
config.setName( "qfieldcloud-sso" );
607+
config.setMethod( "OAuth2" );
608+
config.setConfig( "oauth2config", json.toJson() );
609+
config.setConfig( "qfieldcloud-sso-id", providerId );
610+
QgsApplication::instance()->authManager()->storeAuthenticationConfig( config, true );
611+
612+
mProviderConfigId = config.id();
613+
QSettings().setValue( QStringLiteral( "/QFieldCloud/providerConfigId" ), mProviderConfigId );
614+
emit providerConfigurationChanged();
615+
}
616+
else
617+
{
618+
QgsAuthMethodConfig config;
619+
QgsApplication::instance()->authManager()->loadAuthenticationConfig( mProviderConfigId, config, true );
620+
providerId = config.config( "qfieldcloud-sso-id" );
621+
}
622+
623+
QgsApplication::instance()->authManager()->updateNetworkRequest( request, mProviderConfigId );
624+
request.setRawHeader( "X-QFC-IDP-ID", providerId.toLatin1() );
625+
626+
const QList<QNetworkCookie> cookies = QgsNetworkAccessManager::instance()->cookieJar()->cookiesForUrl( mUrl );
627+
auto match = std::find_if( cookies.begin(), cookies.end(), []( const QNetworkCookie &cookie ) { return cookie.name() == QLatin1String( "csrftoken" ); } );
628+
if ( match != cookies.end() )
629+
{
630+
request.setRawHeader( "X-CSRFToken", match->value() );
631+
request.setRawHeader( "Referer", mUrl.toLatin1() );
632+
}
633+
}
472634
}
473635

474636
void QFieldCloudConnection::setClientHeaders( QNetworkRequest &request )

0 commit comments

Comments
 (0)