Last active
January 23, 2021 16:39
-
-
Save cypres/a197fc9f6dc1ad59f08c to your computer and use it in GitHub Desktop.
Two-legged OAuth 1.0a in C++11
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Two-legged OAuth 1.0a proof of concept | |
// Feel free to use, copy or modify to your own needs. | |
// | |
// Requires glog, gflags and crypto++ (cryptopp.com) | |
// Compile with: | |
// clang++ -std=c++11 -lcryptopp -lglog -lgflags oauth.cc -o oauth_test | |
// Run with: | |
// ./oauth_test -alsologtostderr | |
// | |
// Copyright (C) 2014 OnlineCity Aps <[email protected]> | |
// | |
// Licensed under The MIT License: | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
// THE SOFTWARE. | |
#include <gflags/gflags.h> | |
#include <glog/logging.h> | |
#pragma clang diagnostic push | |
#pragma clang diagnostic ignored "-Wunused-variable" | |
#pragma clang diagnostic ignored "-Wunused-function" | |
#include <cryptopp/cryptlib.h> | |
#include <cryptopp/hmac.h> | |
#include <cryptopp/sha.h> | |
#include <cryptopp/base64.h> | |
#include <cryptopp/secblock.h> | |
#include <cryptopp/filters.h> | |
#pragma clang diagnostic pop | |
#include <algorithm> | |
#include <functional> | |
#include <locale> | |
#include <map> | |
#include <memory> | |
#include <set> | |
#include <string> | |
#include <utility> | |
// Encode following the RFC 5859 (OAuth) section 3.6 | |
std::string OauthUrlEncode(const std::string &str) { | |
static const char *const lut = "0123456789ABCDEF"; | |
std::string out; | |
out.reserve(str.length()*2); | |
for (auto c : str) { | |
if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') { | |
out.push_back(c); | |
} else { | |
out.push_back('%'); | |
out.push_back(lut[c >> 4]); | |
out.push_back(lut[c & 15]); | |
} | |
} | |
out.shrink_to_fit(); | |
return out; | |
} | |
// Decode a RFC3986 encoded string | |
std::string OauthUrlDecode(const std::string &str) { | |
std::string decoded; | |
decoded.reserve(str.length()); | |
for (auto it = str.cbegin(); it != str.cend(); ++it) { | |
auto const c = *it; | |
if ( | |
c == '%' && | |
it+1 != str.cend() && | |
it+2 != str.cend() && | |
std::isxdigit(*(it+1)) && | |
std::isxdigit(*(it+2)) | |
) { | |
std::string hex(3, 0); | |
hex[0] = *(it+1); | |
hex[1] = *(it+2); | |
auto cd = static_cast<std::string::value_type>(std::stol(hex, 0, 16)); | |
decoded.push_back(cd); | |
it += 2; | |
} else if (c == '+') { | |
decoded.push_back(' '); | |
} else { | |
decoded.push_back(c); | |
} | |
} | |
return decoded; | |
} | |
// Convert an URL according to RFC 5849 (OAuth) section 3.4.1.2 + 3.6 | |
// Returns the encoded URL and any query params specified in the URL | |
// Does not do any normalization of the URL. It's assumed the OAuth client already does this. | |
std::pair<std::string,std::string> OauthConvertUrl(const std::string &url) { | |
std::string protocol, host, path, query; | |
// Protocol | |
const std::string protocol_sep = "://"; | |
auto proto_it = std::search(url.cbegin(), url.cend(), protocol_sep.cbegin(), protocol_sep.cend()); | |
protocol.reserve(std::distance(url.cbegin(), proto_it)); | |
std::transform(url.cbegin(), proto_it, std::back_inserter(protocol), (int (*)(int))std::tolower); | |
std::advance(proto_it, protocol_sep.length()); | |
// Host | |
auto path_it = std::find(proto_it, url.cend(), '/'); | |
host.reserve(std::distance(proto_it, path_it)); | |
std::transform(proto_it, path_it, std::back_inserter(host), (int (*)(int))std::tolower); | |
// Path | |
auto query_it = std::find(path_it, url.cend(), '?'); | |
path.assign(path_it, query_it); | |
// Query | |
if (query_it != url.cend()) { | |
++query_it; | |
} | |
query.assign(query_it, url.cend()); | |
// Encode | |
std::string encoded; | |
encoded.append(OauthUrlEncode(protocol)); | |
encoded.append(OauthUrlEncode(protocol_sep)); | |
encoded.append(OauthUrlEncode(host)); | |
encoded.append(OauthUrlEncode(path)); | |
return std::make_pair(encoded, query); | |
} | |
typedef std::map<std::string, std::set<std::string>> OauthParams; | |
// Normalize params into a single string according to RFC 5849 (OAuth) section 3.4.1.3.2 | |
// 1. Name and value is URL encoded | |
// 2. The parameters are sorted by name, using ascending byte value ordering | |
// 3. The name of each parameter is concatenated to its corresponding value using an "=" character | |
// 4. The sorted name/value pairs are concatenated together into a single string by using an "&" character | |
std::pair<std::string,OauthParams> OauthConvertParams(const std::string &url_params, const std::string &post_params, const std::string &oauth_header) { | |
// Use a map with a set, to always keep params sorted in the correct order | |
// (byte value ordering, first by key then value if same key) | |
OauthParams params; | |
// Parse and decode url_params + post_params, inserting them into params | |
std::string key, lower_key, value, signature; | |
bool val = false; | |
for (auto paramstr : { url_params, post_params } ) { | |
for (auto const c : paramstr) { | |
if (c == '=') { | |
val = true; | |
} else if (c == '&') { | |
if (key.compare("oauth_signature") != 0) { | |
params[OauthUrlDecode(key)].insert(OauthUrlDecode(value)); | |
} else { | |
signature = value; | |
} | |
val = false; | |
key.clear(); | |
value.clear(); | |
} else { | |
if (val) { | |
value.push_back(c); | |
} else { | |
key.push_back(c); | |
} | |
} | |
} | |
if (!key.empty()) { | |
if (key.compare("oauth_signature") != 0) { | |
params[OauthUrlDecode(key)].insert(OauthUrlDecode(value)); | |
} | |
key.clear(); | |
value.clear(); | |
} | |
val = false; | |
} | |
// Parse OAuth header | |
const std::string oauth_head_prefix = "OAuth "; | |
auto head_it = std::search(oauth_header.cbegin(), oauth_header.cend(), oauth_head_prefix.cbegin(), oauth_head_prefix.cend()); | |
if (head_it != oauth_header.cend()) { | |
std::advance(head_it, oauth_head_prefix.length()); | |
} | |
while (head_it != oauth_header.cend()) { | |
auto const c = *head_it; | |
if (c == '=') { | |
val = true; | |
++head_it; | |
} else if (++head_it == oauth_header.cend() || c == ',') { | |
// Convert to lowercase so we can compare it | |
lower_key.clear(); | |
lower_key.reserve(key.length()); | |
std::transform(key.cbegin(), key.cend(), std::back_inserter(lower_key), (int (*)(int))std::tolower); | |
// If not oauth_signature or realm (case insensitive) add it | |
if (lower_key.compare("oauth_signature") == 0) { | |
signature = value; | |
} else if (lower_key.compare("realm") != 0) { | |
params[OauthUrlDecode(key)].insert(OauthUrlDecode(value)); | |
} | |
val = false; | |
key.clear(); | |
value.clear(); | |
} else if (c != '"' && !std::isspace(c)) { | |
if (val) { | |
value.push_back(c); | |
} else { | |
key.push_back(c); | |
} | |
} | |
} | |
// Encode and concatenate a string with all params | |
std::string encoded; | |
for (auto it = params.cbegin(); it != params.cend(); ) { | |
auto const k = it->first; | |
for (auto val_it = it->second.cbegin(); val_it != it->second.cend(); ) { | |
auto const v = *val_it; | |
encoded.append(OauthUrlEncode(k)); | |
encoded.push_back('='); | |
encoded.append(OauthUrlEncode(v)); | |
if (++val_it != it->second.cend()) { | |
encoded.push_back('&'); | |
} | |
} | |
if (++it != params.cend()) { | |
encoded.push_back('&'); | |
} | |
} | |
if (!signature.empty()) { | |
params["oauth_signature"].insert(signature); | |
} | |
return std::make_pair(OauthUrlEncode(encoded), params); | |
} | |
bool OauthValidateSignature(const std::string &base_string, const std::string &signature_key, const std::string &given_signature) { | |
using CryptoPP::SecByteBlock; | |
using CryptoPP::HMAC; | |
using CryptoPP::SHA1; | |
using CryptoPP::StringSource; | |
using CryptoPP::StringSink; | |
using CryptoPP::ArraySink; | |
using CryptoPP::Base64Decoder; | |
using CryptoPP::HashVerificationFilter; | |
std::string given_signature_raw; | |
SecByteBlock key(signature_key.length()); | |
key.Assign(reinterpret_cast<const byte*>(signature_key.data()), signature_key.length()); | |
bool result = false; | |
try { | |
StringSource(given_signature, true, new Base64Decoder(new StringSink(given_signature_raw))); | |
HMAC<SHA1> hmac(key, key.size()); | |
const int flags = HashVerificationFilter::PUT_RESULT | HashVerificationFilter::HASH_AT_END; | |
StringSource(base_string + given_signature_raw, true, new HashVerificationFilter(hmac, new ArraySink((byte*)&result, sizeof(result)), flags)); | |
} catch (const CryptoPP::Exception &e) { | |
LOG(DFATAL) << e.what(); | |
} | |
return result; | |
} | |
// Do two-legged oauth using non-standard method ( http://oauth.googlecode.com/svn/spec/ext/consumer_request/1.0/drafts/2/spec.html ) | |
int main(int argc, char **argv) { | |
google::ParseCommandLineFlags(&argc, &argv, true); | |
google::InitGoogleLogging(argv[0]); | |
const std::string authorize_header = "OAuth realm=\"http://provider.example.net/\"," | |
"oauth_consumer_key=\"dpf43f3p2l4k3l03\"," | |
"oauth_signature_method=\"HMAC-SHA1\"," | |
"oauth_signature=\"IxyYZfG2BaKh8JyEGuHCOin%2F4bA%3D\"," | |
"oauth_timestamp=\"1191242096\"," | |
"oauth_token=\"\"," | |
"oauth_nonce=\"kllo9940pd9333jh\"," | |
"oauth_version=\"1.0\""; | |
const std::string http_method = "GET"; | |
const std::string url = "http://provider.example.net/profile"; | |
const std::string request_params; | |
const std::string ip_address = "127.0.0.1"; | |
const std::string secret = "kd94hf93k423kf44"; | |
// Generate a signature base string | |
std::string base_string; | |
for (const auto &c : http_method) { | |
base_string.push_back(std::toupper(c, std::locale::classic())); | |
} | |
base_string.push_back('&'); | |
auto converted_url = OauthConvertUrl(url); | |
base_string.append(converted_url.first); | |
base_string.push_back('&'); | |
auto converted_params = OauthConvertParams(converted_url.second, request_params, authorize_header); | |
base_string.append(converted_params.first); | |
LOG(INFO) << base_string; | |
CHECK_STREQ(base_string.c_str(), "GET&http%3A%2F%2Fprovider.example.net%2Fprofile&" | |
"oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26" | |
"oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3D%26oauth_version%3D1.0"); | |
// Decode the oauth_signature param | |
std::string given_signature = OauthUrlDecode(*converted_params.second["oauth_signature"].cbegin()); | |
// Validate the signature | |
std::string signature_method = OauthUrlDecode(*converted_params.second["oauth_signature_method"].cbegin()); | |
bool signature_valid = false; | |
if (signature_method.compare("HMAC-SHA1") == 0) { | |
// Construct the key used for HMAC-SHA1 signatures (consumer_secret + '&' + token_secret) | |
std::string signature_key(OauthUrlEncode(secret)+'&'); | |
signature_valid = OauthValidateSignature(base_string, signature_key, given_signature); | |
} else if (signature_method.compare("PLAINTEXT") == 0) { | |
// Construct the plaintext signature (consumer_secret + '&' + token_secret) | |
std::string signature_key(OauthUrlEncode(secret)+'&'); | |
signature_valid = signature_key.compare(given_signature) == 0; | |
} else { | |
DLOG(FATAL) << "Unsupported signature method: " << signature_method; | |
} | |
LOG(INFO) << signature_valid; | |
CHECK(signature_valid); | |
// TODO(reader) Validate the timestamp, nonce | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
i have a HMAC already and a key how do i get that working