Remoto - VFS: VFS_google_oauth2.cpp Source File
Remoto - VFS
VFS_google_oauth2.cpp
Go to the documentation of this file.
1 
2 #include <QUrlQuery>
3 #include <QNetworkRequest>
4 
5 #include "VFS.h"
6 #include "VFS_google_oauth2.h"
7 
8 google_oauth2::google_oauth2(QString discovery_uri, QString client_id, QString client_secret, QString redirect_uri, QString tokeninfo_uri, QString hosted_domain, bool debug)
9 : VFS_auth()
10 , _debug(debug)
11 , _client_id(client_id)
12 , _client_secret(client_secret)
13 , _redirect_uri(redirect_uri)
14 , _tokeninfo_uri(tokeninfo_uri)
15 , _hosted_domain(hosted_domain)
16 , _discovery_uri(discovery_uri)
17 //, _discovery_reply(nullptr)
18 , _manager(this)
19 , _valid(false)
20 {
21  if (_debug)
22  {
23  printf("%s\nclient_id: %s\nclient_secret: %s\nredirect_uri: %s\ntokeninfo_uri: %s\n",
24  qUtf8Printable(className()),
25  qUtf8Printable(client_id),
26  qUtf8Printable(client_secret),
27  qUtf8Printable(redirect_uri),
28  qUtf8Printable(tokeninfo_uri)
29  );
30  }
31 
32  connect(VFS::root(),SIGNAL(initialized()),this,SLOT(initialize()));
33 }
34 
36 {
37 }
38 
40 {
41  if (_debug)
42  printf("OAUTH READ: %s\n",qUtf8Printable(r->_data.toJson()));
43 
44  if (!_valid)
45  {
46  //VFS::ERROR( r->_reason = QString("%1 has not initialized.").arg(className()), 0, className() );
47  r->_reason = QString("%1 has not initialized.").arg(className());
48  r->_success = false;
49  return;
50  }
51 
52  QJsonObject d = r->_data.object();
53 
54  bool auth = r->_path == ""; //if the path is empty we're authenticating. Otherwise we're just reading the cached data for a user.
55 
56  if (!auth)
57  {
58  QString user = r->_path;
59 
60  QMutexLocker l(&_lock);
61 
62  if (_userCache.contains(user))
63  {
64  r->_data.setObject( _userCache[user] );
65  r->_success = true;
66  return;
67  }
68  else
69  {
70  r->_success = false;
71  r->_reason = "User data is unavailable for '"+user+"'";
72  return;
73  }
74  }
75 
76  //QString user = d["username"].toString("");
77  //QString pass = d["password"].toString("");
78  QString authcode = d["authtokenonce"].toString("");
79 
80  if (_debug)
81  printf("authcode:'%s' auth: %d\n",qUtf8Printable(authcode),auth);
82 
83  if (authcode.isEmpty())
84  {
85  r->_reason = "No single use authcode present.";
86  r->_success = false;
87  return;
88  }
89 
90  //defer the response as if there were a callback.
91  r->_isCallback = true;
92 
93  google_oauth2_auth *a = new google_oauth2_auth(authcode,this);
94  connect(a, SIGNAL(authResult(google_oauth2_auth *)),
95  this, SLOT(authResult(google_oauth2_auth *)) );
96 
97  _auths[a] = r;
98 
99  a->authenticate();
100 }
101 
102 //revoke:
103 /*
104  https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#oauth-2.0-endpoints
105 curl -d -X -POST --header "Content-type:application/x-www-form-urlencoded" \
106  https://oauth2.googleapis.com/revoke?token={token}
107 */
108 
109 //list:
110 //https://developers.google.com/admin-sdk/directory/v1/reference/groups/list
111 
113 {
114  /*
115  we need to provide for all users:
116  {
117  "username": username,
118  "uidnumber": uid,
119  "realname": realname
120  }
121  */
122 /*
123  if (_debug)
124  printf("filter group: %s\n",qUtf8Printable(_group));
125 
126  QJsonObject o;
127  struct passwd *pw;
128 
129  setpwent();
130  while((pw = getpwent()) != nullptr)
131  {
132  if (_debug)
133  printf("name=%s uid=%d group=%d real=%s\n",pw->pw_name,pw->pw_uid,pw->pw_gid,pw->pw_gecos);
134 
135  // kind of inefficient that we have to fetch each user,
136  // then fetch all groups for each user,
137  // then make sure that user is in the group that we want,
138  // but there doesn't seem to be a get all "users in group" mechanism
139 
140  if (_group.isEmpty() || getGroups(pw).contains(_group))
141  {
142  QString username(pw->pw_name);
143  int uidnumber((int)pw->pw_uid);
144  QString realname(pw->pw_gecos);
145  if (realname.isEmpty())
146  realname = username;
147 
148  o[username] = QJsonObject {
149  { "username", username },
150  { "uidnumber", uidnumber },
151  { "realname", realname }
152  };
153  }
154  }
155  endpwent();
156 
157  QJsonObject u = r->_data.object();
158  u[r->_initialPath] = o;
159  r->_data.setObject(u);
160  r->_success = true;
161 
162  if (_debug)
163  printf("USERS: %s",qUtf8Printable( r->_data.toJson() ));
164 */
165  r->_success = false;
166 }
167 
169 {
170  return _client_id;
171 }
172 
173 void google_oauth2::discoverySslErrors(const QList<QSslError> &errors)
174 {
175  for (int i=0;i<errors.size();i++)
176  VFS::ERROR( errors.at(i).errorString(), 0, className() );
177 
178  //discoveryFinished();
179  _discovery_reply->deleteLater();
180 }
181 
183 {
184  VFS::LOG("Discovery request completed: "+_discovery_reply->url().toString(QUrl::FullyEncoded),9,className());
185 
186  if (_discovery_reply->error())
187  {
188  VFS::ERROR( _discovery_reply->errorString(), 0, className() );
189  _valid = false;
190  }
191  else
192  {
193  QByteArray data = _discovery_reply->readAll();
194 
195  if (_debug)
196  printf("Discovery response: %s\n",qUtf8Printable(data));
197 
198  QJsonParseError error;
199  QJsonDocument doc = QJsonDocument::fromJson(data,&error);
200  if (error.error == QJsonParseError::NoError)
201  {
202  _discovery_data = doc.object();
203 
204  //confirm the fields we want are there
205  if ( //_discovery_data.contains("authorization_endpoint") &&
206  _discovery_data.contains("token_endpoint")
207  )
208  {
209  _valid = true;
210  }
211  else
212  {
213  VFS::ERROR( "Discovery response was missing a required field.", 0, className() );
214  _valid = false;
215  }
216  }
217  else
218  {
219  VFS::ERROR( QString("Failed to parse discovery response: '%1' : %2\n%3").arg(_discovery_reply->url().toString()).arg(error.errorString()).arg(QString(data)), 0, className() );
220  _valid = false;
221  }
222  }
223 
224  _discovery_reply->deleteLater();
225  _discovery_reply = nullptr;
226 }
227 
229 {
230  //https://developers.google.com/identity/protocols/oauth2/openid-connect#discovery
231 
232  //fetch the discovery document. We can't continue until this fetch is complete and successful.
233  QNetworkRequest request(_discovery_uri);
234  _discovery_reply = _manager.get(request);
235 
236  connect( _discovery_reply, SIGNAL(finished()),
237  this, SLOT(discoveryFinished()) );
238 
239  connect( _discovery_reply, SIGNAL(sslErrors(QList<QSslError>)),
240  this, SLOT(discoverySslErrors(QList<QSslError>)) );
241 }
242 
244 {
245  if (_auths.contains(auth))
246  {
247  VFS_request *r = _auths.take(auth);
248 
249  if (auth->_authenticated)
250  {
251  QJsonObject d = r->_data.object();
252 
253  d["username"] = auth->_username;
254  d["uidnumber"] = auth->_id;
255  d["realname"] = auth->_realname;
256  d["groups"] = auth->_groups;
257 
258  _userCache[auth->_username] = d; //cache the result. This is a little sloppy, but let's get the car on the road.
259 
260  r->_data.setObject(d);
261  r->_success = true;
262  }
263  else
264  { r->_reason = auth->_reason;
265  r->_success = false;
266  }
267 
268  issueResponse(r);
269  }
270  else
271  {
272  VFS::ERROR("Received an auth response but can't associate with a VFS_request.");
273  }
274 }
275 
276 
277 
279 : QObject(auth)
280 , _auth(auth)
281 , _authcode(authcode)
282 //, _csrf(QUuid::createUuid())
283 , _authenticated(false)
284 , _reason("Unspecified reason")
285 , _reply(nullptr)
286 {
287 }
288 
290 {
291  if (_reply)
292  _reply->deleteLater();
293  _reply = nullptr;
294 }
295 
297 {
298  QUrlQuery params;
299 
300  params.addQueryItem( "client_id", _auth->_client_id );
301  params.addQueryItem( "client_secret", _auth->_client_secret );
302  params.addQueryItem( "grant_type", "authorization_code");
303  params.addQueryItem( "code",_authcode );
304  params.addQueryItem( "redirect_uri", _auth->_redirect_uri );
305 
306  QNetworkRequest request(_auth->_discovery_data["token_endpoint"].toString());
307  //request.setRawHeader("X-Requested-With",_csrf.toByteArray());
308  request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
309  //request.setAttribute(QNetworkRequest::RedirectionTargetAttribute ,QNetworkRequest::NoLessSafeRedirectPolicy);
310  request.setAttribute(QNetworkRequest::FollowRedirectsAttribute,true);
311 
312  _reply = _auth->_manager.post(request,params.query().toUtf8());
313 
314  connect( _reply, SIGNAL(finished()),
315  this, SLOT(authResponse()) );
316 
317  connect( _reply, SIGNAL(sslErrors(QList<QSslError>)),
318  this, SLOT(sslErrors(QList<QSslError>)) );
319 
320  connect( _reply, SIGNAL(redirected(const QUrl &)),
321  this, SLOT(redirected(const QUrl &)) );
322 }
323 
325 {
326  //this is an error url the that developer will need to follow for more information
327  printf("REDIRECTED: %s\n",qUtf8Printable(url.toString()));
328 }
329 
330 void google_oauth2_auth::sslErrors(const QList<QSslError> &errors)
331 {
332  for (int i=0;i<errors.size();i++)
333  VFS::ERROR( errors.at(i).errorString(), 0, _auth->className() );
334 
335  _reply->deleteLater();
336  _reply = nullptr;
337 }
338 
340 {
341  QByteArray data = _reply->readAll();
342 
343  if (_auth->_debug)
344  {
345  //VFS::WARN("Authentication Request completed: "+_reply->url().toString(QUrl::FullyEncoded),9,_auth->className());
346  printf( "Authentication Request completed: %s\n", qUtf8Printable(_reply->url().toString(QUrl::FullyEncoded)));
347  printf( "Status: %d\n",_reply->error());
348 
349  printf("\nHeaders:\n");
350  const QList<QNetworkReply::RawHeaderPair> headerPairs = _reply->rawHeaderPairs();
351  foreach (QNetworkReply::RawHeaderPair h,headerPairs)
352  printf(" %s ::: %s\n", qUtf8Printable(h.first), qUtf8Printable(h.second) );
353 
354  printf("\nAuthentication response: %s\n",qUtf8Printable(data));
355  }
356 
357  if (_reply->error())
358  {
359  //VFS::ERROR( _reason = _reply->errorString(), 0, _auth->className() );
360  _reason = _reply->errorString();
361  _authenticated = false;
362  }
363  else
364  {
365 // if (!_reply->hasRawHeader("X-Requested-With") || _reply->rawHeader("X-Requested-With")!=_csrf.toByteArray())
366 // {
367 // _authenticated = false;
368 // _reason = "Missing or incorrect header: 'X-Requested-With'";
369 // }
370 // else
371  {
372  //QByteArray data = _reply->readAll();
373  QJsonParseError error;
374  QJsonDocument doc = QJsonDocument::fromJson(data,&error);
375 
376  if (error.error == QJsonParseError::NoError)
377  {
378  //free the old one
379  _reply->deleteLater();
380  _reply = nullptr;
381 
382  //we should now have something like:
383  //{
384  // "access_token": "ya29.a0AfH6SMDKdMkGpbcJwonCaqzGu_Sk7ydTsbCHUBg7Fo3YrBUxyqdmgIvacj-6Bp1dnX0uSGa9bz_iIUMR8FDEzZmb_QNU9XtwGnkpzTik3AG-KYB-e1XXEWrWHfdASm-tP3KfLRY3NbyMBPdK1y3h6MZ28HmXPeG0luv5YKMpImwQ",
385  // "expires_in": 3598,
386  // "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email openid",
387  // "token_type": "Bearer",
388  // "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRlZGMwMTJkMDdmNTJhZWRmZDVmOTc3ODRlMWJjYmUyM2MxOTcyNGQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIzNDgyODgxMjk5OTItZTJnbm1sYmVhczhxOG9rZm5tc3Y4ZW50YjB2MGx0dGsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIzNDgyODgxMjk5OTItZTJnbm1sYmVhczhxOG9rZm5tc3Y4ZW50YjB2MGx0dGsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDgwNDMxNzI1MzY3NDM4Nzg4MDAiLCJoZCI6ImV5ZWxhc2guYWkiLCJlbWFpbCI6ImNocmlzQGV5ZWxhc2guYWkiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6InJZQnV0RF9LUFNKaS00OW5qLVBXUXciLCJuYW1lIjoiQ2hyaXMgSGVhbGVyIiwicGljdHVyZSI6Imh0dHBzOi8vbGg2Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tOG5JWjZPcV9fQzgvQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQU1adXVjbDhvcFhtajdrYjZua2Y2MFM5aXE2dUxscHZCUS9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiQ2hyaXMiLCJmYW1pbHlfbmFtZSI6IkhlYWxlciIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNjA2MzA2MTYwLCJleHAiOjE2MDYzMDk3NjB9.Zivl6KpdZmZqq36bSl_yXGcxzdEDDsqFLPTZttzwNsERzZw9l5T9zsg7qf3i51u5M5zG1TnkjyqpmZEYLUt3_2FgD_0IyMxSxJpFh1yAWOFZoA_g-8rkrVKVJ9i_8XTb96ZpAfyDCQLVEgAdAj2CEAkNw8BwoaN5M4b6Q4ZvEg5LeSLMSjiai9G4abXOn5rl1Xd6iUn6SKFKORJlaWWGelzB4qHQfUzsHAr5F49rETQZ0Wblde9Y3pYaXYtOH-BRbV2v4VLPateEXTVGM3BbB_CZ2hh0IRHi6w7kvTVwTBIkXjniIurbC_clERN07VVHPnBFEQ-n_msBSPqjyR3a_g"
389  //}
390  QJsonObject d = doc.object();
391 
392  _id_token = d["id_token"].toString();
393  _access_token = d["access_token"].toString();
394 
395  QNetworkRequest request( _auth->_tokeninfo_uri ); //not provided by discovery, may change, and advised we don't rely on their debug link, but for a small number of users, fuck it.
396  request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
397  //request.setAttribute(QNetworkRequest::RedirectionTargetAttribute ,QNetworkRequest::NoLessSafeRedirectPolicy);
398  request.setAttribute(QNetworkRequest::FollowRedirectsAttribute,true);
399 
400  QUrlQuery params;
401  params.addQueryItem("id_token",_id_token);
402  _reply = _auth->_manager.post(request,params.query().toUtf8());
403 
404  connect( _reply, SIGNAL(finished()),
405  this, SLOT(tokenResponse()) );
406 
407  connect( _reply, SIGNAL(sslErrors(QList<QSslError>)),
408  this, SLOT(sslErrors(QList<QSslError>)) );
409 
410  connect( _reply, SIGNAL(redirected(const QUrl &)),
411  this, SLOT(redirected(const QUrl &)) );
412 
413  return;
414  }
415  else
416  {
417  //VFS::ERROR( _reason = QString("Failed to parse authResponse: '%1' : %2\n%3").arg(_reply->url().toString()).arg(error.errorString()).arg(QString(data)), 0, _auth->className() );
418  _reason = QString("Failed to parse authResponse: '%1' : %2\n%3").arg(_reply->url().toString()).arg(error.errorString()).arg(QString(data));
419  _authenticated = false;
420  }
421  }
422  }
423 
424  emit authResult(this);
425  deleteLater();
426 }
427 
429 {
430  QByteArray data = _reply->readAll();
431 
432  if (_auth->_debug)
433  { //VFS::WARN("Tokeninfo Request completed: "+_reply->url().toString(QUrl::FullyEncoded),9,_auth->className());
434  printf( "Tokeninfo Request completed: %s\n", qUtf8Printable(_reply->url().toString(QUrl::FullyEncoded)));
435  printf( "Status: %d\n",_reply->error());
436 
437  printf("\nHeaders:\n");
438  const QList<QNetworkReply::RawHeaderPair> headerPairs = _reply->rawHeaderPairs();
439  foreach (QNetworkReply::RawHeaderPair h,headerPairs)
440  printf(" %s ::: %s\n", qUtf8Printable(h.first), qUtf8Printable(h.second) );
441 
442  printf("\nAuthentication response: %s\n",qUtf8Printable(data));
443  }
444 
445  if (_reply->error())
446  {
447  //VFS::ERROR( _reason = _reply->errorString(), 0, _auth->className() );
448  _reason = _reply->errorString();
449  _authenticated = false;
450  }
451  else
452  {
453  QJsonParseError error;
454  QJsonDocument doc = QJsonDocument::fromJson(data,&error);
455 
456  if (error.error == QJsonParseError::NoError)
457  {
458  QJsonObject o = doc.object();
459 
460  if (o["aud"].toString() == _auth->_client_id)
461  {
462  _authenticated = true;
463  _username = o["email"].toString();
464  _realname = o["name"].toString();
465  _id = o["sub"].toString();
466 
467  _groups = QJsonArray(); //FIXME
468  _groups << "staff";
469  }
470  else
471  {
472  //VFS::ERROR( _reason = QString("Failed to parse tokeninfo response: '%1' : %2\n%3").arg(_reply->url().toString()).arg(error.errorString()).arg(QString(data)), 0, _auth->className() );
473  _reason = "Tokeninfo response did not contain the correct client_id";
474  _authenticated = false;
475  }
476  }
477  else
478  {
479  //VFS::ERROR( _reason = QString("Failed to parse tokeninfo response: '%1' : %2\n%3").arg(_reply->url().toString()).arg(error.errorString()).arg(QString(data)), 0, _auth->className() );
480  _reason = QString("Failed to parse tokeninfo response: '%1' : %2\n%3").arg(_reply->url().toString()).arg(error.errorString()).arg(QString(data));
481  _authenticated = false;
482  }
483  }
484 
485  emit authResult(this);
486  deleteLater();
487 }
488 
The base class for authenticating users.
Definition: VFS_auth.h:7
virtual void issueResponse(VFS_request *t)
Once a request has been completed, issue a response.
Definition: VFS_node.cpp:1981
QString className()
Return the class name of a node.
Definition: VFS_node.cpp:2039
QMutex _lock
A recursive mutex that is local to this node.
Definition: VFS_node.h:178
void finished(bool andDelete=false)
Emitted if a thread fails to create its node, or a node is rm()'d, or any other reason a node has com...
The base class for all requests between nodes.
Definition: VFS_node.h:54
QString _reason
if something (probably bad) happened, this is the reason
Definition: VFS_node.h:108
QString _path
the target path remnant... the remaining path element once the request has found its target
Definition: VFS_node.h:95
bool _isCallback
whether or not to issue a response (IE, another request is chained to this request,...
Definition: VFS_node.h:98
bool _success
if the request was successfully completed
Definition: VFS_node.h:107
QJsonDocument _data
the request payload
Definition: VFS_node.h:102
static VFS * root()
Return the root node of the VFS filesystem.
Definition: VFS.cpp:399
static void LOG(QString message, int level=0, QString user="server")
Send a message to the VFS::_messages VFS_stream.
Definition: VFS.cpp:209
static void ERROR(QString message, int level=0, QString user="server")
Send a message to the VFS::_errors VFS_stream.
Definition: VFS.cpp:307
QNetworkReply * _reply
void redirected(const QUrl &url)
virtual void sslErrors(const QList< QSslError > &errors)
void authResult(google_oauth2_auth *auth)
google_oauth2_auth(QString authcode, google_oauth2 *auth)
google_oauth2 * _auth
QString _client_secret
QJsonObject _discovery_data
QMap< QString, QJsonObject > _userCache
QString _tokeninfo_uri
virtual void read(VFS_request *r)
Return the data contents of this node, or if it's a container call ls()
friend class google_oauth2_auth
virtual void discoverySslErrors(const QList< QSslError > &errors)
QMap< google_oauth2_auth *, VFS_request * > _auths
Q_INVOKABLE google_oauth2(QString discovery_uri, QString client_id, QString client_secret, QString redirect_uri, QString tokeninfo_uri, QString hosted_domain, bool debug=false)
void authResult(google_oauth2_auth *auth)
virtual void ls(VFS_request *r)
List the contents of this node.
virtual QString reportDetails()
Additional details for a generated report.
QNetworkAccessManager _manager
QNetworkReply * _discovery_reply
virtual ~google_oauth2()
url(value, options)