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