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