1 /*
2     xoauth2provider.cpp - X-OAuth2 provider for QCA
3 
4     Copyright (c) 2016 by Pali Rohár <pali.rohar@gmail.com>
5 
6     *************************************************************************
7     *                                                                       *
8     * This library is free software; you can redistribute it and/or         *
9     * modify it under the terms of the GNU General Public                   *
10     * License as published by the Free Software Foundation; either          *
11     * version 2 of the License, or (at your option) any later version.      *
12     *                                                                       *
13     *************************************************************************
14 */
15 
16 #include "xoauth2provider.h"
17 
18 #include <QNetworkAccessManager>
19 #include <QNetworkRequest>
20 #include <QNetworkReply>
21 #include <qca.h>
22 
23 #include <QJsonDocument>
24 #include <QUrlQuery>
25 
26 class XOAuth2SASLContext : public QCA::SASLContext
27 {
28     Q_OBJECT
29 
30 private:
31     QString user;
32     QString clientId;
33     QString requestUrl;
34     QCA::SecureArray clientSecretKey;
35     QCA::SecureArray refreshToken;
36     QCA::SecureArray accessToken;
37 
38     QByteArray data;
39     QByteArray result_to_net;
40     QByteArray result_to_app;
41     QCA::SASLContext::Result result_;
42     QCA::SASL::AuthCondition authCondition_;
43 
44     QNetworkAccessManager *manager;
45 
46 public:
XOAuth2SASLContext(QCA::Provider * p)47     XOAuth2SASLContext(QCA::Provider *p) : QCA::SASLContext(p)
48     {
49         manager = new QNetworkAccessManager(this);
50         reset();
51     }
52 
~XOAuth2SASLContext()53     virtual ~XOAuth2SASLContext()
54     {
55         reset();
56     }
57 
clone() const58     QCA::Provider::Context *clone() const Q_DECL_OVERRIDE
59     {
60         XOAuth2SASLContext *s = new XOAuth2SASLContext(provider());
61         s->user = user;
62         s->clientId = clientId;
63         s->clientSecretKey = clientSecretKey;
64         s->refreshToken = refreshToken;
65         s->accessToken = accessToken;
66         s->requestUrl = requestUrl;
67         return s;
68     }
69 
reset()70     void reset() Q_DECL_OVERRIDE
71     {
72         user.clear();
73         clientId.clear();
74         clientSecretKey.clear();
75         refreshToken.clear();
76         accessToken.clear();
77         requestUrl.clear();
78         data.clear();
79         authCondition_ = QCA::SASL::AuthFail;
80     }
81 
setup(const QString &,const QString &,const QCA::SASLContext::HostPort *,const QCA::SASLContext::HostPort *,const QString &,int)82     void setup(const QString &, const QString &, const QCA::SASLContext::HostPort *, const QCA::SASLContext::HostPort *, const QString &, int) Q_DECL_OVERRIDE
83     {
84     }
85 
setConstraints(QCA::SASL::AuthFlags,int,int)86     void setConstraints(QCA::SASL::AuthFlags, int, int) Q_DECL_OVERRIDE
87     {
88     }
89 
startClient(const QStringList & mechlist,bool)90     void startClient(const QStringList &mechlist, bool) Q_DECL_OVERRIDE
91     {
92         if (!mechlist.contains(QStringLiteral("X-OAUTH2"))) {
93             qWarning("No X-OAUTH2 auth method");
94             authCondition_ = QCA::SASL::NoMechanism;
95             QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
96             return;
97         }
98         authCondition_ = QCA::SASL::AuthFail;
99         result_ = QCA::SASLContext::Continue;
100         data.clear();
101         tryAgain();
102     }
103 
startServer(const QString &,bool)104     void startServer(const QString &, bool) Q_DECL_OVERRIDE
105     {
106         result_ = QCA::SASLContext::Error;
107         QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
108     }
109 
serverFirstStep(const QString &,const QByteArray *)110     void serverFirstStep(const QString &, const QByteArray *) Q_DECL_OVERRIDE
111     {
112         result_ = QCA::SASLContext::Error;
113         QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
114     }
115 
nextStep(const QByteArray &)116     void nextStep(const QByteArray &) Q_DECL_OVERRIDE
117     {
118         tryAgain();
119     }
120 
tryAgain()121     void tryAgain() Q_DECL_OVERRIDE
122     {
123         if (user.isEmpty() || (accessToken.isEmpty() && (clientId.isEmpty() || clientSecretKey.isEmpty() || requestUrl.isEmpty() || refreshToken.isEmpty()))) {
124             result_ = QCA::SASLContext::Params;
125             QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
126             return;
127         }
128         if (accessToken.isEmpty()) {
129             requestAccessToken();
130             return;
131         }
132         sendAuth();
133     }
134 
update(const QByteArray & from_net,const QByteArray & from_app)135     void update(const QByteArray &from_net, const QByteArray &from_app) Q_DECL_OVERRIDE
136     {
137         result_to_app = from_net;
138         result_to_net = from_app;
139         result_ = QCA::SASLContext::Success;
140         QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
141     }
142 
waitForResultsReady(int)143     bool waitForResultsReady(int) Q_DECL_OVERRIDE
144     {
145         return true;
146     }
147 
result() const148     QCA::SASLContext::Result result() const Q_DECL_OVERRIDE
149     {
150         return result_;
151     }
152 
mechlist() const153     QStringList mechlist() const Q_DECL_OVERRIDE
154     {
155         return QStringList();
156     }
157 
mech() const158     QString mech() const Q_DECL_OVERRIDE
159     {
160         return QStringLiteral("X-OAUTH2");
161     }
162 
haveClientInit() const163     bool haveClientInit() const Q_DECL_OVERRIDE
164     {
165         return false;
166     }
167 
stepData() const168     QByteArray stepData() const Q_DECL_OVERRIDE
169     {
170         return data;
171     }
172 
to_net()173     QByteArray to_net() Q_DECL_OVERRIDE
174     {
175         return result_to_net;
176     }
177 
encoded() const178     int encoded() const Q_DECL_OVERRIDE
179     {
180         return result_to_net.size();
181     }
182 
to_app()183     QByteArray to_app() Q_DECL_OVERRIDE
184     {
185         return result_to_app;
186     }
187 
ssf() const188     int ssf() const Q_DECL_OVERRIDE
189     {
190         return 0;
191     }
192 
authCondition() const193     QCA::SASL::AuthCondition authCondition() const Q_DECL_OVERRIDE
194     {
195         return authCondition_;
196     }
197 
clientParams() const198     QCA::SASL::Params clientParams() const Q_DECL_OVERRIDE
199     {
200         bool needUser = user.isEmpty();
201         bool needPass = (accessToken.isEmpty() && (clientId.isEmpty() || clientSecretKey.isEmpty() || requestUrl.isEmpty() || refreshToken.isEmpty()));
202         return QCA::SASL::Params(needUser, false, needPass, false);
203     }
204 
setClientParams(const QString * userParam,const QString *,const QCA::SecureArray * passParam,const QString *)205     void setClientParams(const QString *userParam, const QString *, const QCA::SecureArray *passParam, const QString *) Q_DECL_OVERRIDE
206     {
207         if (userParam) {
208             user = *userParam;
209         }
210         if (passParam) {
211             const QList<QByteArray> &params = passParam->toByteArray().split(0x7F);
212             if (params.size() == 5) {
213                 clientId = QString::fromUtf8(params.at(0));
214                 clientSecretKey = params.at(1);
215                 refreshToken = params.at(2);
216                 accessToken = params.at(3);
217                 requestUrl = QString::fromUtf8(params.at(4));
218             } else {
219                 clientId.clear();
220                 clientSecretKey.clear();
221                 refreshToken.clear();
222                 requestUrl.clear();
223                 if (params.size() == 1) {
224                     accessToken = params.at(0);
225                 } else {
226                     accessToken.clear();
227                 }
228             }
229         }
230     }
231 
realmlist() const232     QStringList realmlist() const Q_DECL_OVERRIDE
233     {
234         return QStringList();
235     }
236 
username() const237     QString username() const Q_DECL_OVERRIDE
238     {
239         return QString();
240     }
241 
authzid() const242     QString authzid() const Q_DECL_OVERRIDE
243     {
244         return QString();
245     }
246 
247 private:
requestAccessToken()248     void requestAccessToken()
249     {
250         QUrlQuery query;
251         query.addQueryItem(QStringLiteral("client_id"), clientId);
252         query.addQueryItem(QStringLiteral("client_secret"), QString::fromUtf8(clientSecretKey.toByteArray()));
253         query.addQueryItem(QStringLiteral("refresh_token"), QString::fromUtf8(refreshToken.toByteArray()));
254         query.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("refresh_token"));
255         const QByteArray &data = query.toString(QUrl::FullyEncoded).toUtf8();
256         QNetworkRequest request(requestUrl);
257         request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
258         QNetworkReply *reply = manager->post(request, data);
259         connect(reply, SIGNAL(finished()), this, SLOT(accessTokenReceived()));
260     }
261 
sendAuth()262     void sendAuth()
263     {
264         if (accessToken.isEmpty()) {
265             authCondition_ = QCA::SASL::AuthFail;
266             result_ = QCA::SASLContext::Error;
267         } else {
268             data.clear();
269             data += '\0';
270             data += user.toUtf8();
271             data += '\0';
272             data += accessToken.toByteArray();
273             result_ = QCA::SASLContext::Success;
274         }
275         QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
276     }
277 
278 private Q_SLOTS:
accessTokenReceived()279     void accessTokenReceived()
280     {
281         QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
282         const QByteArray &replyData = reply->readAll();
283         reply->deleteLater();
284         QJsonParseError error;
285         const QJsonDocument &replyJson = QJsonDocument::fromJson(replyData, &error);
286         bool ok = (error.error == QJsonParseError::NoError);
287         const QVariant &replyVariant = replyJson.toVariant();
288         if (ok && replyVariant.type() == QVariant::Map) {
289             const QVariantMap &replyMap = replyVariant.toMap();
290             if (replyMap.contains(QStringLiteral("access_token"))) {
291                 const QVariant &accessTokenVariant = replyMap.value(QStringLiteral("access_token"));
292                 if (accessTokenVariant.type() == QVariant::String) {
293                     accessToken = accessTokenVariant.toString().toUtf8();
294                 } else {
295                     ok = false;
296                 }
297             } else {
298                 ok = false;
299             }
300             if (!ok || replyMap.contains(QStringLiteral("error"))) {
301                 const QString &error = replyMap.value(QStringLiteral("error")).toString();
302                 const QString &errorDescription = replyMap.value(QStringLiteral("error_description")).toString();
303                 qWarning("requestAccessToken failed, error: %s, description: %s", error.toUtf8().data(), errorDescription.toUtf8().data());
304                 accessToken.clear();
305             }
306         } else {
307             qWarning("requestAccessToken failed, invalid reply: %s", replyData.data());
308             accessToken.clear();
309         }
310         sendAuth();
311     }
312 };
313 
314 class QCAXOAuth2SASL : public QCA::Provider
315 {
316 public:
QCAXOAuth2SASL()317     QCAXOAuth2SASL()
318     {
319     }
320 
~QCAXOAuth2SASL()321     virtual ~QCAXOAuth2SASL()
322     {
323     }
324 
init()325     void init() Q_DECL_OVERRIDE
326     {
327     }
328 
qcaVersion() const329     int qcaVersion() const Q_DECL_OVERRIDE
330     {
331         return QCA_VERSION;
332     }
333 
name() const334     QString name() const Q_DECL_OVERRIDE
335     {
336         return QStringLiteral("xoauth2sasl");
337     }
338 
features() const339     QStringList features() const Q_DECL_OVERRIDE
340     {
341         return QStringList(QStringLiteral("sasl"));
342     }
343 
createContext(const QString & type)344     QCA::Provider::Context *createContext(const QString &type) Q_DECL_OVERRIDE
345     {
346         if (type == QLatin1String("sasl")) {
347             return new XOAuth2SASLContext(this);
348         }
349         return NULL;
350     }
351 };
352 
createProviderXOAuth2()353 QCA::Provider *createProviderXOAuth2()
354 {
355     return new QCAXOAuth2SASL();
356 }
357 
358 #include "xoauth2provider.moc"
359