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