1 /** 2 * ae.net.oauth.common 3 * 4 * I have no idea what I'm doing. 5 * Please don't use this module. 6 * 7 * License: 8 * This Source Code Form is subject to the terms of 9 * the Mozilla Public License, v. 2.0. If a copy of 10 * the MPL was not distributed with this file, You 11 * can obtain one at http://mozilla.org/MPL/2.0/. 12 * 13 * Authors: 14 * Vladimir Panteleev <ae@cy.md> 15 */ 16 17 module ae.net.oauth.common; 18 19 import std.algorithm.sorting; 20 import std.base64; 21 import std.conv; 22 import std.datetime; 23 import std.digest.hmac; 24 import std.digest.sha; 25 26 import ae.net.ietf.url; 27 import ae.utils.text; 28 29 debug(OAUTH) import std.stdio : stderr; 30 31 /// OAuth configuration. 32 struct OAuthConfig 33 { 34 string consumerKey; /// 35 string consumerSecret; /// 36 } 37 38 /** 39 Implements an OAuth client session. 40 41 Example: 42 --- 43 OAuthSession session; 44 session.config.consumerKey = "(... obtain from service ...)"; 45 session.config.consumerSecret = "(... obtain from service ...)"; 46 session.token = "(... obtain from service ...)"; 47 session.tokenSecret = "(... obtain from service ...)"; 48 49 ... 50 51 UrlParameters parameters; 52 parameters["payload"] = "(... some data here ...)"; 53 auto request = new HttpRequest; 54 auto queryString = parameters.byPair.map!(p => session.encode(p.key) ~ "=" ~ session.encode(p.value)).join("&"); 55 auto baseURL = "https://api.example.com/endpoint"; 56 auto fullURL = baseURL ~ "?" ~ queryString; 57 request.resource = fullURL; 58 request.method = "POST"; 59 request.headers["Authorization"] = session.prepareRequest(baseURL, "POST", parameters).oauthHeader; 60 httpRequest(request, null); 61 --- 62 */ 63 struct OAuthSession 64 { 65 OAuthConfig config; /// 66 67 /// 68 string token, tokenSecret; 69 70 /// Signs a request and returns the relevant parameters for the "Authorization" header. 71 UrlParameters prepareRequest(string requestUrl, string method, UrlParameters[] parameters...) 72 { 73 UrlParameters oauthParams; 74 oauthParams["oauth_consumer_key"] = config.consumerKey; 75 oauthParams["oauth_token"] = token; 76 oauthParams["oauth_timestamp"] = Clock.currTime().toUnixTime().text(); 77 oauthParams["oauth_nonce"] = randomString(); 78 oauthParams["oauth_version"] = "1.0"; 79 oauthParams["oauth_signature_method"] = "HMAC-SHA1"; 80 oauthParams["oauth_signature"] = signRequest(method, requestUrl, parameters ~ oauthParams); 81 return oauthParams; 82 } 83 84 /// Calculates the signature for a request. 85 string signRequest(string method, string requestUrl, UrlParameters[] parameters...) 86 { 87 string paramStr; 88 bool[string] keys; 89 90 foreach (set; parameters) 91 foreach (key, value; set) 92 keys[key] = true; 93 94 foreach (key; keys.keys.sort()) 95 { 96 string[] values; 97 foreach (set; parameters) 98 foreach (value; set.valuesOf(key).sort()) 99 values ~= value; 100 101 foreach (value; values.sort()) 102 { 103 if (paramStr.length) 104 paramStr ~= '&'; 105 paramStr ~= encode(key) ~ "=" ~ encode(value); 106 } 107 } 108 109 auto str = encode(method) ~ "&" ~ encode(requestUrl) ~ "&" ~ encode(paramStr); 110 debug(OAUTH) stderr.writeln("Signature base string: ", str); 111 112 auto key = encode(config.consumerSecret) ~ "&" ~ encode(tokenSecret); 113 debug(OAUTH) stderr.writeln("Signing key: ", key); 114 auto digest = hmac!SHA1(cast(ubyte[])str, cast(ubyte[])key); 115 return Base64.encode(digest); 116 } 117 118 unittest 119 { 120 // Example from https://dev.twitter.com/oauth/overview/creating-signatures 121 122 OAuthSession session; 123 session.config.consumerSecret = "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw"; 124 session.tokenSecret = "LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE"; 125 126 UrlParameters getVars, postVars, oauthVars; 127 getVars["include_entities"] = "true"; 128 postVars["status"] = "Hello Ladies + Gentlemen, a signed OAuth request!"; 129 130 oauthVars["oauth_consumer_key"] = "xvz1evFS4wEEPTGEFPHBog"; 131 oauthVars["oauth_nonce"] = "kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg"; 132 oauthVars["oauth_signature_method"] = "HMAC-SHA1"; 133 oauthVars["oauth_timestamp"] = "1318622958"; 134 oauthVars["oauth_token"] = "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb"; 135 oauthVars["oauth_version"] = "1.0"; 136 137 auto signature = session.signRequest("POST", "https://api.twitter.com/1/statuses/update.json", getVars, postVars, oauthVars); 138 assert(signature == "tnnArxj06cWHq44gCs1OSKk/jLY="); 139 } 140 141 /// Alias to `oauthEncode`. 142 alias encode = oauthEncode; 143 } 144 145 /// Converts OAuth parameters into a string suitable for the "Authorization" header. 146 string oauthHeader(UrlParameters oauthParams) 147 { 148 string s; 149 foreach (key, value; oauthParams) 150 s ~= (s.length ? ", " : "") ~ key ~ `="` ~ oauthEncode(value) ~ `"`; 151 return "OAuth " ~ s; 152 } 153 154 static import std.ascii; 155 /// Performs URL encoding as required by OAuth. 156 static alias oauthEncode = encodeUrlPart!(c => std.ascii.isAlphaNum(c) || c=='-' || c=='.' || c=='_' || c=='~');