PageRenderTime 228ms CodeModel.GetById 60ms app.highlight 129ms RepoModel.GetById 32ms app.codeStats 0ms

/mordor/http/oauth.cpp

http://github.com/mozy/mordor
C++ | 400 lines | 353 code | 34 blank | 13 comment | 89 complexity | 048564e3f00cced2a565df42f74135dc MD5 | raw file
  1// Copyright (c) 2009 - Mozy, Inc.
  2
  3#include "oauth.h"
  4
  5#include "client.h"
  6#include "mordor/date_time.h"
  7#include "mordor/streams/stream.h"
  8
  9namespace Mordor {
 10namespace HTTP {
 11namespace OAuth {
 12
 13static void writeBody(ClientRequest::ptr request, const std::string &body)
 14{
 15    request->requestStream()->write(body.c_str(), body.size());
 16    request->requestStream()->close();
 17}
 18
 19std::pair<std::string, std::string>
 20extractCredentials(ClientRequest::ptr request)
 21{
 22    URI::QueryString responseParams = request->responseStream();
 23    std::pair<std::string, std::string> result;
 24    std::pair<URI::QueryString::iterator, URI::QueryString::iterator> its =
 25        responseParams.equal_range("oauth_token");
 26    if (its.first == responseParams.end() || its.first->second.empty())
 27        MORDOR_THROW_EXCEPTION(InvalidResponseException("Missing oauth_token in response",
 28            request));
 29    result.first = its.first->second;
 30    ++its.first;
 31    if (its.first != its.second)
 32        MORDOR_THROW_EXCEPTION(InvalidResponseException("Duplicate oauth_token in response",
 33            request));
 34    its = responseParams.equal_range("oauth_token_secret");
 35    if (its.first == responseParams.end() || its.first->second.empty())
 36        MORDOR_THROW_EXCEPTION(InvalidResponseException("Missing oauth_token_secret in response",
 37            request));
 38    result.second = its.first->second;
 39    ++its.first;
 40    if (its.first != its.second)
 41        MORDOR_THROW_EXCEPTION(InvalidResponseException("Duplicate oauth_token_secret in response",
 42            request));
 43    return result;
 44}
 45
 46std::pair<std::string, std::string>
 47getTemporaryCredentials(RequestBroker::ptr requestBroker, const URI &uri,
 48    const std::string &method, const std::string &signatureMethod,
 49    const std::pair<std::string, std::string> &clientCredentials,
 50    const URI &callbackUri)
 51{
 52    MORDOR_ASSERT(requestBroker);
 53    MORDOR_ASSERT(uri.isDefined());
 54    MORDOR_ASSERT(method == GET || method == POST);
 55    MORDOR_ASSERT(signatureMethod == "PLAINTEXT" || signatureMethod == "HMAC-SHA1");
 56    MORDOR_ASSERT(!clientCredentials.first.empty());
 57    MORDOR_ASSERT(!clientCredentials.second.empty());
 58    URI::QueryString oauthParameters;
 59
 60    oauthParameters.insert(std::make_pair("oauth_consumer_key", clientCredentials.first));
 61    oauthParameters.insert(std::make_pair("oauth_version", "1.0"));
 62    if (!callbackUri.isDefined())
 63        oauthParameters.insert(std::make_pair("oauth_callback", "oob"));
 64    else
 65        oauthParameters.insert(std::make_pair("oauth_callback", callbackUri.toString()));
 66    nonceAndTimestamp(oauthParameters);
 67    sign(uri, method, signatureMethod, clientCredentials.second, std::string(),
 68        oauthParameters);
 69
 70    Request requestHeaders;
 71    requestHeaders.requestLine.method = method;
 72    requestHeaders.requestLine.uri = uri;
 73    std::string body;
 74    if (method == GET) {
 75        // Add parameters that are part of the request token URI
 76        URI::QueryString qsFromUri = uri.queryString();
 77        oauthParameters.insert(qsFromUri.begin(), qsFromUri.end());
 78        requestHeaders.requestLine.uri.query(oauthParameters);
 79    } else {
 80        body = oauthParameters.toString();
 81        requestHeaders.entity.contentType.type = "application";
 82        requestHeaders.entity.contentType.subtype = "x-www-form-urlencoded";
 83        requestHeaders.entity.contentLength = body.size();
 84    }
 85
 86    boost::function<void (ClientRequest::ptr)> bodyDg;
 87    if (!body.empty())
 88        bodyDg = boost::bind(&writeBody, _1, boost::cref(body));
 89    ClientRequest::ptr request;
 90    try {
 91        request = requestBroker->request(requestHeaders, false, bodyDg);
 92    } catch (...) {
 93        throw;
 94    }
 95    if (request->response().status.status != OK)
 96        MORDOR_THROW_EXCEPTION(InvalidResponseException(request));
 97
 98    return extractCredentials(request);
 99}
100
101std::pair<std::string, std::string>
102getTokenCredentials(RequestBroker::ptr requestBroker, const URI &uri,
103    const std::string &method, const std::string &signatureMethod,
104    const std::pair<std::string, std::string> &clientCredentials,
105    const std::pair<std::string, std::string> &temporaryCredentials,
106    const std::string &verifier)
107{
108    MORDOR_ASSERT(requestBroker);
109    MORDOR_ASSERT(uri.isDefined());
110    MORDOR_ASSERT(method == GET || method == POST);
111    MORDOR_ASSERT(signatureMethod == "PLAINTEXT" || signatureMethod == "HMAC-SHA1");
112    MORDOR_ASSERT(!clientCredentials.first.empty());
113    MORDOR_ASSERT(!clientCredentials.second.empty());
114    MORDOR_ASSERT(!temporaryCredentials.first.empty());
115    MORDOR_ASSERT(!temporaryCredentials.second.empty());
116    URI::QueryString oauthParameters;
117
118    oauthParameters.insert(std::make_pair("oauth_consumer_key", clientCredentials.first));
119    oauthParameters.insert(std::make_pair("oauth_token", temporaryCredentials.first));
120    oauthParameters.insert(std::make_pair("oauth_verifier", verifier));
121    oauthParameters.insert(std::make_pair("oauth_version", "1.0"));
122    nonceAndTimestamp(oauthParameters);
123    sign(uri, method, signatureMethod, clientCredentials.second,
124        temporaryCredentials.second, oauthParameters);
125
126    Request requestHeaders;
127    requestHeaders.requestLine.method = method;
128    requestHeaders.requestLine.uri = uri;
129    std::string body;
130    if (method == GET) {
131        // Add parameters that are part of the request token URI
132        URI::QueryString qsFromUri = uri.queryString();
133        oauthParameters.insert(qsFromUri.begin(), qsFromUri.end());
134        requestHeaders.requestLine.uri.query(oauthParameters);
135    } else {
136        body = oauthParameters.toString();
137        requestHeaders.entity.contentType.type = "application";
138        requestHeaders.entity.contentType.subtype = "x-www-form-urlencoded";
139        requestHeaders.entity.contentLength = body.size();
140    }
141
142    boost::function<void (ClientRequest::ptr)> bodyDg;
143    if (!body.empty())
144        bodyDg = boost::bind(&writeBody, _1, boost::cref(body));
145    ClientRequest::ptr request;
146    try {
147        request = requestBroker->request(requestHeaders, false, bodyDg);
148    } catch (...) {
149        throw;
150    }
151    if (request->response().status.status != OK)
152        MORDOR_THROW_EXCEPTION(InvalidResponseException(request));
153
154    return extractCredentials(request);
155}
156
157void
158authorize(Request &nextRequest, const std::string &signatureMethod,
159    const std::pair<std::string, std::string> &clientCredentials,
160    const std::pair<std::string, std::string> &tokenCredentials,
161    const std::string &realm, const std::string &scheme)
162{
163    MORDOR_ASSERT(signatureMethod == "PLAINTEXT" || signatureMethod == "HMAC-SHA1");
164    MORDOR_ASSERT(!clientCredentials.first.empty());
165    MORDOR_ASSERT(!clientCredentials.second.empty());
166    MORDOR_ASSERT(!tokenCredentials.first.empty());
167    MORDOR_ASSERT(!tokenCredentials.second.empty());
168
169    AuthParams &authorization = nextRequest.request.authorization;
170    authorization.scheme = "OAuth";
171    authorization.param.clear();
172    authorization.parameters.clear();
173    authorization.parameters["oauth_consumer_key"] = clientCredentials.first;
174    authorization.parameters["oauth_token"] = tokenCredentials.first;
175    authorization.parameters["oauth_version"] = "1.0";
176    nonceAndTimestamp(authorization.parameters);
177    URI uri = nextRequest.requestLine.uri;
178    if (!uri.authority.hostDefined()) {
179        MORDOR_ASSERT(!scheme.empty());
180        std::string fullUri = scheme + "://" + nextRequest.request.host +
181            uri.toString();
182        uri = fullUri;
183    }
184    sign(uri, nextRequest.requestLine.method, signatureMethod,
185        clientCredentials.second, tokenCredentials.second,
186        authorization.parameters);
187    if (!realm.empty())
188        authorization.parameters["realm"] = realm;
189    // OAuth is stupid, and doesn't trust quoted-string in the Authorization
190    // header, so we have to use their encoding method (which is really just
191    // URI encoding, only encoding characters not in the unreserved character
192    // set).
193    // Note that technically Mordor breaks the OAuth spec because when it
194    // serializes, it will only add quotes when necessary, whereas OAuth
195    // (again, very naively) requires quotes on all values.
196    for (StringMap::iterator it = authorization.parameters.begin();
197        it != authorization.parameters.end();
198        ++it) {
199        it->second = URI::encode(it->second);
200    }
201}
202
203template <class T>
204void nonceAndTimestamp(T &oauthParameters)
205{
206    static const char *allowedChars =
207        "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
208
209    std::string nonce;
210    nonce.resize(40);
211    for (size_t i = 0; i < 40; ++i) {
212        nonce[i] = allowedChars[rand() % 36];
213    }
214
215    typename T::iterator it = oauthParameters.find("oauth_timestamp");
216    if (it != oauthParameters.end())
217        oauthParameters.erase(it);
218    it = oauthParameters.find("oauth_nonce");
219    if (it != oauthParameters.end())
220        oauthParameters.erase(it);
221    oauthParameters.insert(std::make_pair("oauth_timestamp",
222        boost::lexical_cast<std::string>(toTimeT(
223            boost::posix_time::second_clock::universal_time()))));
224    oauthParameters.insert(std::make_pair("oauth_nonce", nonce));
225}
226
227template void nonceAndTimestamp<StringMap>(StringMap &);
228template void nonceAndTimestamp<URI::QueryString>(URI::QueryString &);
229
230template <class T>
231std::string
232generateSignature(const URI &uri, const std::string &method,
233    const std::string &clientSecret, const std::string &tokenSecret,
234    const T &oauthParameters, const URI::QueryString &postParameters)
235{
236    MORDOR_ASSERT(oauthParameters.find("oauth_signature_method") != oauthParameters.end());
237
238    typename T::const_iterator it;
239
240    std::ostringstream os;
241    URI requestUri(uri);
242    requestUri.queryDefined(false);
243    requestUri.fragmentDefined(false);
244    requestUri.normalize();
245    os << method << '&' << URI::encode(requestUri.toString());
246    std::multiset<std::pair<std::string, std::string> > combined;
247    std::multiset<std::pair<std::string, std::string> >::iterator
248        combinedIt;
249    for (it = oauthParameters.begin(); it != oauthParameters.end(); ++it)
250        if (stricmp(it->first.c_str(), "realm") != 0 &&
251            stricmp(it->first.c_str(), "oauth_signature") != 0)
252            combined.insert(*it);
253    URI::QueryString::const_iterator it2;
254    for (it2 = postParameters.begin(); it2 != postParameters.end(); ++it2)
255        combined.insert(*it2);
256    if (uri.queryDefined()) {
257        URI::QueryString queryParams = uri.queryString();
258        for (it2 = queryParams.begin(); it2 != queryParams.end(); ++it2)
259            combined.insert(*it2);
260    }
261
262    os << '&';
263    std::string signatureBaseString = os.str();
264    os.str("");
265    bool first = true;
266    for (combinedIt = combined.begin();
267        combinedIt != combined.end();
268        ++combinedIt) {
269        if (!first)
270            os << '&';
271        first = false;
272        os << URI::encode(combinedIt->first)
273            << '=' << URI::encode(combinedIt->second);
274    }
275    signatureBaseString.append(URI::encode(os.str()));
276
277    std::string secrets = URI::encode(clientSecret);
278    secrets.append(1, '&');
279    secrets.append(URI::encode(tokenSecret));
280
281    it = oauthParameters.find("oauth_signature_method");
282    const std::string &signatureMethod = it->second;
283    if (stricmp(signatureMethod.c_str(), "HMAC-SHA1") == 0) {
284        return base64encode(hmacSha1(signatureBaseString, secrets));
285    } else if (stricmp(signatureMethod.c_str(), "PLAINTEXT") == 0) {
286        return secrets;
287    } else {
288        MORDOR_NOTREACHED();
289    }
290}
291
292template <class T>
293void sign(const URI &uri, const std::string &method,
294    const std::string &signatureMethod, const std::string &clientSecret,
295    const std::string &tokenSecret, T &oauthParameters,
296    const URI::QueryString &postParameters)
297{
298    std::pair<typename T::iterator, typename T::iterator> its =
299        oauthParameters.equal_range("oauth_signature_method");
300    if (its.first == oauthParameters.end() ||
301        boost::next(its.first) != its.second ||
302        its.first->second != signatureMethod) {
303        oauthParameters.erase(its.first, its.second);
304        oauthParameters.insert(std::make_pair("oauth_signature_method",
305            signatureMethod));
306    }
307    its = oauthParameters.equal_range("oauth_signature");
308    oauthParameters.erase(its.first, its.second);
309    oauthParameters.insert(std::make_pair("oauth_signature",
310        generateSignature(uri, method, clientSecret, tokenSecret,
311        oauthParameters, postParameters)));
312}
313
314template void sign<StringMap>(const URI &, const std::string &,
315    const std::string &, const std::string &, const std::string &, StringMap &,
316    const URI::QueryString &);
317template void sign<URI::QueryString>(const URI &, const std::string &,
318    const std::string &, const std::string &, const std::string &,
319    URI::QueryString &, const URI::QueryString &);
320
321template <class T>
322bool validate(const URI &uri, const std::string &method,
323    const std::string &clientSecret, const std::string &tokenSecret,
324    const T &oauthParameters,
325    const URI::QueryString &postParameters)
326{
327    std::pair<typename T::const_iterator, typename T::const_iterator> its =
328        oauthParameters.equal_range("oauth_signature_method");
329    // Not exactly one signature method
330    if (its.first == oauthParameters.end() ||
331        boost::next(its.first) != its.second)
332        return false;
333    // Unsupported signature method
334    if (stricmp(its.first->second.c_str(), "PLAINTEXT") != 0 &&
335        stricmp(its.first->second.c_str(), "HMAC-SHA1") != 0)
336        return false;
337    its = oauthParameters.equal_range("oauth_signature");
338    // Not exactly one signature
339    if (its.first == oauthParameters.end() ||
340        boost::next(its.first) != its.second)
341        return false;
342    return its.first->second == generateSignature(uri, method,
343        clientSecret, tokenSecret, oauthParameters, postParameters);
344}
345
346template bool validate<StringMap>(const URI &, const std::string &,
347    const std::string &, const std::string &, const StringMap &,
348    const URI::QueryString &);
349template bool validate<URI::QueryString>(const URI &, const std::string &,
350    const std::string &, const std::string &, const URI::QueryString &,
351    const URI::QueryString &);
352
353static void authorizeDg(ClientRequest::ptr request,
354    const std::string &signatureMethod,
355    const std::pair<std::string, std::string> &clientCredentials,
356    const std::pair<std::string, std::string> &tokenCredentials,
357    const std::string &realm, const std::string scheme,
358    boost::function<void (ClientRequest::ptr)> bodyDg)
359{
360    authorize(request->request(), signatureMethod, clientCredentials,
361        tokenCredentials, realm, scheme);
362    if (bodyDg)
363        bodyDg(request);
364    else
365        request->doRequest();
366}
367
368ClientRequest::ptr
369RequestBroker::request(Request &requestHeaders, bool forceNewConnection,
370    boost::function<void (ClientRequest::ptr)> bodyDg)
371{
372    ClientRequest::ptr priorRequest;
373    std::pair<std::string, std::string> clientCredentials, tokenCredentials;
374    std::string signatureMethod, realm;
375    size_t attempts = 0;
376    while (true) {
377        boost::function<void (ClientRequest::ptr)> wrappedBodyDg = bodyDg;
378        if (m_getCredentialsDg(requestHeaders.requestLine.uri, priorRequest,
379            signatureMethod, clientCredentials, tokenCredentials, realm,
380            attempts++))
381            wrappedBodyDg = boost::bind(&authorizeDg, _1,
382                boost::cref(signatureMethod), boost::cref(clientCredentials),
383                boost::cref(tokenCredentials), boost::cref(realm),
384                requestHeaders.requestLine.uri.scheme(),
385                bodyDg);
386        else if (priorRequest)
387            return priorRequest;
388        if (priorRequest)
389            priorRequest->finish();
390        priorRequest = parent()->request(requestHeaders, forceNewConnection,
391            wrappedBodyDg);
392        if (priorRequest->response().status.status == UNAUTHORIZED &&
393            isAcceptable(priorRequest->response().response.wwwAuthenticate,
394                "OAuth"))
395            continue;
396        return priorRequest;
397    }
398}
399
400}}}