1 /***************************************************************************
2 begin : August 1, 2016
3 copyright : (C) 2016 by Monsanto Company, USA
4 author : Larry Shaffer, Boundless Spatial
5 email : lshaffer at boundlessgeo dot com
6 ***************************************************************************
7 * *
8 * This program is free software; you can redistribute it and/or modify *
9 * it under the terms of the GNU General Public License as published by *
10 * the Free Software Foundation; either version 2 of the License, or *
11 * (at your option) any later version. *
12 * *
13 ***************************************************************************/
14
15 #include "qgso2.h"
16
17 #include "o0globals.h"
18 #include "o0settingsstore.h"
19 #include "o2replyserver.h"
20 #include "qgsapplication.h"
21 #include "qgsauthoauth2config.h"
22 #include "qgslogger.h"
23 #include "qgsnetworkaccessmanager.h"
24 #include "qgsblockingnetworkrequest.h"
25
26 #include <QDir>
27 #include <QJsonDocument>
28 #include <QJsonObject>
29 #include <QSettings>
30 #include <QUrl>
31 #include <QUrlQuery>
32
33 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
34 #include <QRandomGenerator>
35 #endif
36
37 QString QgsO2::O2_OAUTH2_STATE = QStringLiteral( "state" );
38
QgsO2(const QString & authcfg,QgsAuthOAuth2Config * oauth2config,QObject * parent,QNetworkAccessManager * manager)39 QgsO2::QgsO2( const QString &authcfg, QgsAuthOAuth2Config *oauth2config,
40 QObject *parent, QNetworkAccessManager *manager )
41 : O2( parent, manager )
42 , mTokenCacheFile( QString() )
43 , mAuthcfg( authcfg )
44 , mOAuth2Config( oauth2config )
45 {
46 initOAuthConfig();
47 }
48
~QgsO2()49 QgsO2::~QgsO2()
50 {
51 // FIXME: This crashes app on QgsApplication destruction
52 // Verify that objects are actually being deleted via QgsAuthManager's destruction
53 //mOAuth2Config->deleteLater();
54
55 if ( mTokenCacheFile.startsWith( QgsAuthOAuth2Config::tokenCacheDirectory( true ) )
56 && QFile::exists( mTokenCacheFile ) )
57 {
58 if ( !QFile::remove( mTokenCacheFile ) )
59 {
60 QgsDebugMsg( QStringLiteral( "Could not remove temp token cache file: %1" ).arg( mTokenCacheFile ) );
61 }
62 }
63 }
64
initOAuthConfig()65 void QgsO2::initOAuthConfig()
66 {
67 if ( !mOAuth2Config )
68 {
69 return;
70 }
71
72 // common properties to all grant flows
73 const QString localpolicy = QStringLiteral( "http://127.0.0.1:% 1/%1" ).arg( mOAuth2Config->redirectUrl() ).replace( QLatin1String( "% 1" ), QLatin1String( "%1" ) );
74 QgsDebugMsgLevel( QStringLiteral( "localpolicy(w/port): %1" ).arg( localpolicy.arg( mOAuth2Config->redirectPort() ) ), 2 );
75 setLocalhostPolicy( localpolicy );
76 setLocalPort( mOAuth2Config->redirectPort() );
77 mIsLocalHost = isLocalHost( QUrl( localpolicy.arg( mOAuth2Config->redirectPort() ) ) );
78
79 setTokenUrl( mOAuth2Config->tokenUrl() );
80 // refresh token url is marked as optional -- we use the token url if user has not specified a specific refresh URL
81 setRefreshTokenUrl( !mOAuth2Config->refreshTokenUrl().isEmpty() ? mOAuth2Config->refreshTokenUrl() : mOAuth2Config->tokenUrl() );
82
83 setScope( mOAuth2Config->scope() );
84 // TODO: add support to O2 (or this class?) for state query param
85
86 // common optional properties
87 setApiKey( mOAuth2Config->apiKey() );
88 setExtraRequestParams( mOAuth2Config->queryPairs() );
89
90 switch ( mOAuth2Config->grantFlow() )
91 {
92 case QgsAuthOAuth2Config::AuthCode:
93 setGrantFlow( O2::GrantFlowAuthorizationCode );
94 setRequestUrl( mOAuth2Config->requestUrl() );
95 setClientId( mOAuth2Config->clientId() );
96 setClientSecret( mOAuth2Config->clientSecret() );
97
98 break;
99 case QgsAuthOAuth2Config::Implicit:
100 setGrantFlow( O2::GrantFlowImplicit );
101 setRequestUrl( mOAuth2Config->requestUrl() );
102 setClientId( mOAuth2Config->clientId() );
103
104 break;
105 case QgsAuthOAuth2Config::ResourceOwner:
106 setGrantFlow( O2::GrantFlowResourceOwnerPasswordCredentials );
107 setClientId( mOAuth2Config->clientId() );
108 setClientSecret( mOAuth2Config->clientSecret() );
109 setUsername( mOAuth2Config->username() );
110 setPassword( mOAuth2Config->password() );
111
112 break;
113 }
114
115 setSettingsStore( mOAuth2Config->persistToken() );
116
117 setVerificationResponseContent();
118 }
119
setSettingsStore(bool persist)120 void QgsO2::setSettingsStore( bool persist )
121 {
122 mTokenCacheFile = QgsAuthOAuth2Config::tokenCachePath( mAuthcfg, !persist );
123
124 QSettings *settings = new QSettings( mTokenCacheFile, QSettings::IniFormat );
125 O0SettingsStore *store = new O0SettingsStore( settings, O2_ENCRYPTION_KEY );
126 store->setGroupKey( QStringLiteral( "authcfg_%1" ).arg( mAuthcfg ) );
127 setStore( store );
128 }
129
setVerificationResponseContent()130 void QgsO2::setVerificationResponseContent()
131 {
132 QFile verhtml( QStringLiteral( ":/oauth2method/oauth2_verification_finished.html" ) );
133 if ( verhtml.open( QIODevice::ReadOnly | QIODevice::Text ) )
134 {
135 setReplyContent( verhtml.readAll() );
136 }
137 }
138
isLocalHost(const QUrl redirectUrl) const139 bool QgsO2::isLocalHost( const QUrl redirectUrl ) const
140 {
141 const QString hostName = redirectUrl.host();
142 if ( hostName == QLatin1String( "localhost" ) || hostName == QLatin1String( "127.0.0.1" ) || hostName == QLatin1String( "[::1]" ) )
143 {
144 return true;
145 }
146 return false;
147 }
148
149 // slot
clearProperties()150 void QgsO2::clearProperties()
151 {
152 // TODO: clear object properties
153 }
154
onSetAuthCode(const QString & code)155 void QgsO2::onSetAuthCode( const QString &code )
156 {
157 setCode( code );
158 onVerificationReceived( QMap<QString, QString>() );
159 }
160
link()161 void QgsO2::link()
162 {
163 QgsDebugMsgLevel( QStringLiteral( "QgsO2::link" ), 4 );
164
165 if ( linked() )
166 {
167 QgsDebugMsgLevel( QStringLiteral( "QgsO2::link: Linked already" ), 4 );
168 emit linkingSucceeded();
169 return;
170 }
171
172 setLinked( false );
173 setToken( QString() );
174 setTokenSecret( QString() );
175 setExtraTokens( QVariantMap() );
176 setRefreshToken( QString() );
177 setExpires( 0 );
178
179 if ( grantFlow_ == GrantFlowAuthorizationCode || grantFlow_ == GrantFlowImplicit )
180 {
181 if ( mIsLocalHost )
182 {
183 // Start listening to authentication replies
184 replyServer_->listen( QHostAddress::Any, localPort_ );
185
186 // Save redirect URI, as we have to reuse it when requesting the access token
187 redirectUri_ = localhostPolicy_.arg( replyServer_->serverPort() );
188 }
189 // Assemble initial authentication URL
190 QList<QPair<QString, QString> > parameters;
191 parameters.append( qMakePair( QString( O2_OAUTH2_RESPONSE_TYPE ), ( grantFlow_ == GrantFlowAuthorizationCode ) ?
192 QString( O2_OAUTH2_GRANT_TYPE_CODE ) :
193 QString( O2_OAUTH2_GRANT_TYPE_TOKEN ) ) );
194 parameters.append( qMakePair( QString( O2_OAUTH2_CLIENT_ID ), clientId_ ) );
195 parameters.append( qMakePair( QString( O2_OAUTH2_REDIRECT_URI ), redirectUri_ ) );
196 if ( !scope_.isEmpty() )
197 parameters.append( qMakePair( QString( O2_OAUTH2_SCOPE ), scope_ ) );
198 if ( !state_.isEmpty() )
199 parameters.append( qMakePair( O2_OAUTH2_STATE, state_ ) );
200 if ( !apiKey_.isEmpty() )
201 parameters.append( qMakePair( QString( O2_OAUTH2_API_KEY ), apiKey_ ) );
202
203 for ( auto iter = extraReqParams_.constBegin(); iter != extraReqParams_.constEnd(); ++iter )
204 {
205 parameters.append( qMakePair( iter.key(), iter.value().toString() ) );
206 }
207
208 // Show authentication URL with a web browser
209 QUrl url( requestUrl_ );
210 QUrlQuery query( url );
211 query.setQueryItems( parameters );
212 url.setQuery( query );
213 QgsDebugMsgLevel( QStringLiteral( "QgsO2::link: Emit openBrowser %1" ).arg( url.toString() ), 4 );
214 QgsNetworkAccessManager::instance()->requestAuthOpenBrowser( url );
215 if ( !mIsLocalHost )
216 {
217 emit getAuthCode();
218 }
219 }
220 else if ( grantFlow_ == GrantFlowResourceOwnerPasswordCredentials )
221 {
222 QList<O0RequestParameter> parameters;
223 parameters.append( O0RequestParameter( O2_OAUTH2_CLIENT_ID, clientId_.toUtf8() ) );
224 parameters.append( O0RequestParameter( O2_OAUTH2_CLIENT_SECRET, clientSecret_.toUtf8() ) );
225 parameters.append( O0RequestParameter( O2_OAUTH2_USERNAME, username_.toUtf8() ) );
226 parameters.append( O0RequestParameter( O2_OAUTH2_PASSWORD, password_.toUtf8() ) );
227 parameters.append( O0RequestParameter( O2_OAUTH2_GRANT_TYPE, O2_OAUTH2_GRANT_TYPE_PASSWORD ) );
228 if ( !scope_.isEmpty() )
229 parameters.append( O0RequestParameter( O2_OAUTH2_SCOPE, scope_.toUtf8() ) );
230 if ( !apiKey_.isEmpty() )
231 parameters.append( O0RequestParameter( O2_OAUTH2_API_KEY, apiKey_.toUtf8() ) );
232
233
234 for ( auto iter = extraReqParams_.constBegin(); iter != extraReqParams_.constEnd(); ++iter )
235 {
236 parameters.append( O0RequestParameter( iter.key().toUtf8(), iter.value().toString().toUtf8() ) );
237 }
238
239
240 const QByteArray payload = O0BaseAuth::createQueryParameters( parameters );
241
242 const QUrl url( tokenUrl_ );
243 QNetworkRequest tokenRequest( url );
244 QgsSetRequestInitiatorClass( tokenRequest, QStringLiteral( "QgsO2" ) );
245 tokenRequest.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/x-www-form-urlencoded" ) );
246 QNetworkReply *tokenReply = getManager()->post( tokenRequest, payload );
247
248 connect( tokenReply, SIGNAL( finished() ), this, SLOT( onTokenReplyFinished() ), Qt::QueuedConnection );
249 connect( tokenReply, SIGNAL( error( QNetworkReply::NetworkError ) ), this, SLOT( onTokenReplyError( QNetworkReply::NetworkError ) ), Qt::QueuedConnection );
250 }
251 }
252
253
setState(const QString &)254 void QgsO2::setState( const QString & )
255 {
256 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
257 qsrand( QTime::currentTime().msec() );
258 state_ = QString::number( qrand() );
259 #else
260 state_ = QString::number( QRandomGenerator::system()->generate() );
261 #endif
262 Q_EMIT stateChanged();
263 }
264
265
onVerificationReceived(QMap<QString,QString> response)266 void QgsO2::onVerificationReceived( QMap<QString, QString> response )
267 {
268 QgsDebugMsgLevel( QStringLiteral( "QgsO2::onVerificationReceived: Emitting closeBrowser()" ), 4 );
269 QgsNetworkAccessManager::instance()->requestAuthCloseBrowser();
270
271 if ( mIsLocalHost )
272 {
273 if ( response.contains( QStringLiteral( "error" ) ) )
274 {
275 QgsDebugMsgLevel( QStringLiteral( "QgsO2::onVerificationReceived: Verification failed: %1" ).arg( response["error"] ), 4 );
276 emit linkingFailed();
277 return;
278 }
279
280 if ( !state_.isEmpty() )
281 {
282 if ( response.contains( QStringLiteral( "state" ) ) )
283 {
284 if ( response.value( QStringLiteral( "state" ), QStringLiteral( "ignore" ) ) != state_ )
285 {
286 QgsDebugMsgLevel( QStringLiteral( "QgsO2::onVerificationReceived: Verification failed: (Response returned wrong state)" ), 3 ) ;
287 emit linkingFailed();
288 return;
289 }
290 }
291 else
292 {
293 QgsDebugMsgLevel( QStringLiteral( "QgsO2::onVerificationReceived: Verification failed: (Response does not contain state)" ), 3 );
294 emit linkingFailed();
295 return;
296 }
297 }
298 // Save access code
299 setCode( response.value( QString( O2_OAUTH2_GRANT_TYPE_CODE ) ) );
300 }
301
302 if ( grantFlow_ == GrantFlowAuthorizationCode )
303 {
304
305 // Exchange access code for access/refresh tokens
306 QString query;
307 if ( !apiKey_.isEmpty() )
308 query = QStringLiteral( "?=%1" ).arg( QString( O2_OAUTH2_API_KEY ), apiKey_ );
309 QNetworkRequest tokenRequest( QUrl( tokenUrl_.toString() + query ) );
310 QgsSetRequestInitiatorClass( tokenRequest, QStringLiteral( "QgsO2" ) );
311 tokenRequest.setHeader( QNetworkRequest::ContentTypeHeader, O2_MIME_TYPE_XFORM );
312 QMap<QString, QString> parameters;
313 parameters.insert( O2_OAUTH2_GRANT_TYPE_CODE, code() );
314 parameters.insert( O2_OAUTH2_CLIENT_ID, clientId_ );
315 parameters.insert( O2_OAUTH2_CLIENT_SECRET, clientSecret_ );
316 parameters.insert( O2_OAUTH2_REDIRECT_URI, redirectUri_ );
317 parameters.insert( O2_OAUTH2_GRANT_TYPE, O2_AUTHORIZATION_CODE );
318 const QByteArray data = buildRequestBody( parameters );
319 QNetworkReply *tokenReply = getManager()->post( tokenRequest, data );
320 timedReplies_.add( tokenReply );
321 connect( tokenReply, &QNetworkReply::finished, this, &QgsO2::onTokenReplyFinished, Qt::QueuedConnection );
322 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
323 connect( tokenReply, qOverload<QNetworkReply::NetworkError>( &QNetworkReply::error ), this, &QgsO2::onTokenReplyError, Qt::QueuedConnection );
324 #else
325 connect( tokenReply, &QNetworkReply::errorOccurred, this, &QgsO2::onTokenReplyError, Qt::QueuedConnection );
326 #endif
327 }
328 else if ( grantFlow_ == GrantFlowImplicit )
329 {
330 // Check for mandatory tokens
331 if ( response.contains( O2_OAUTH2_ACCESS_TOKEN ) )
332 {
333 qDebug() << "O2::onVerificationReceived: Access token returned for implicit flow";
334 setToken( response.value( O2_OAUTH2_ACCESS_TOKEN ) );
335 if ( response.contains( O2_OAUTH2_EXPIRES_IN ) )
336 {
337 bool ok = false;
338 const int expiresIn = response.value( O2_OAUTH2_EXPIRES_IN ).toInt( &ok );
339 if ( ok )
340 {
341 qDebug() << "O2::onVerificationReceived: Token expires in" << expiresIn << "seconds";
342 setExpires( QDateTime::currentMSecsSinceEpoch() / 1000 + expiresIn );
343 }
344 }
345 setLinked( true );
346 Q_EMIT linkingSucceeded();
347 }
348 else
349 {
350 qWarning() << "O2::onVerificationReceived: Access token missing from response for implicit flow";
351 Q_EMIT linkingFailed();
352 }
353 }
354 else
355 {
356 setToken( response.value( O2_OAUTH2_ACCESS_TOKEN ) );
357 setRefreshToken( response.value( O2_OAUTH2_REFRESH_TOKEN ) );
358 }
359 }
360
getManager()361 QNetworkAccessManager *QgsO2::getManager()
362 {
363 return QgsNetworkAccessManager::instance();
364 }
365
366 /// Parse JSON data into a QVariantMap
parseTokenResponse(const QByteArray & data)367 static QVariantMap parseTokenResponse( const QByteArray &data )
368 {
369 QJsonParseError err;
370 const QJsonDocument doc = QJsonDocument::fromJson( data, &err );
371 if ( err.error != QJsonParseError::NoError )
372 {
373 qWarning() << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString();
374 return QVariantMap();
375 }
376
377 if ( !doc.isObject() )
378 {
379 qWarning() << "parseTokenResponse: Token response is not an object";
380 return QVariantMap();
381 }
382
383 return doc.object().toVariantMap();
384 }
385
386 // Code adapted from O2::refresh(), but using QgsBlockingNetworkRequest
refreshSynchronous()387 void QgsO2::refreshSynchronous()
388 {
389 qDebug() << "O2::refresh: Token: ..." << refreshToken().right( 7 );
390
391 if ( refreshToken().isEmpty() )
392 {
393 qWarning() << "O2::refresh: No refresh token";
394 onRefreshError( QNetworkReply::AuthenticationRequiredError );
395 return;
396 }
397 if ( refreshTokenUrl_.isEmpty() )
398 {
399 qWarning() << "O2::refresh: Refresh token URL not set";
400 onRefreshError( QNetworkReply::AuthenticationRequiredError );
401 return;
402 }
403
404 QNetworkRequest refreshRequest( refreshTokenUrl_ );
405 refreshRequest.setHeader( QNetworkRequest::ContentTypeHeader, O2_MIME_TYPE_XFORM );
406 QMap<QString, QString> parameters;
407 parameters.insert( O2_OAUTH2_CLIENT_ID, clientId_ );
408 parameters.insert( O2_OAUTH2_CLIENT_SECRET, clientSecret_ );
409 parameters.insert( O2_OAUTH2_REFRESH_TOKEN, refreshToken() );
410 parameters.insert( O2_OAUTH2_GRANT_TYPE, O2_OAUTH2_REFRESH_TOKEN );
411
412 const QByteArray data = buildRequestBody( parameters );
413
414 QgsBlockingNetworkRequest blockingRequest;
415 const QgsBlockingNetworkRequest::ErrorCode errCode = blockingRequest.post( refreshRequest, data, true );
416 if ( errCode == QgsBlockingNetworkRequest::NoError )
417 {
418 const QByteArray reply = blockingRequest.reply().content();
419 const QVariantMap tokens = parseTokenResponse( reply );
420 if ( tokens.contains( QStringLiteral( "error" ) ) )
421 {
422 qDebug() << " Error refreshing token" << tokens.value( QStringLiteral( "error" ) ).toMap().value( QStringLiteral( "message" ) ).toString().toLocal8Bit().constData();
423 unlink();
424 }
425 else
426 {
427 setToken( tokens.value( O2_OAUTH2_ACCESS_TOKEN ).toString() );
428 setExpires( QDateTime::currentMSecsSinceEpoch() / 1000 + tokens.value( O2_OAUTH2_EXPIRES_IN ).toInt() );
429 const QString refreshToken = tokens.value( O2_OAUTH2_REFRESH_TOKEN ).toString();
430 if ( !refreshToken.isEmpty() )
431 setRefreshToken( refreshToken );
432 setLinked( true );
433 qDebug() << " New token expires in" << expires() << "seconds";
434 emit linkingSucceeded();
435 }
436 emit refreshFinished( QNetworkReply::NoError );
437 }
438 else
439 {
440 unlink();
441 qDebug() << "O2::onRefreshFinished: Error" << blockingRequest.errorMessage();
442 emit refreshFinished( blockingRequest.reply().error() );
443 }
444 }
445
computeExpirationDelay()446 void QgsO2::computeExpirationDelay()
447 {
448 const int lExpires = expires();
449 mExpirationDelay = lExpires > 0 ? lExpires - static_cast<int>( QDateTime::currentMSecsSinceEpoch() / 1000 ) : 0;
450 }
451