OAuth multipart post in Erlang - twitter

I am trying to extend this library https://github.com/tim/erlang-oauth-examples to allow the client to make multipart posts (as necessary for Twitter's statuses/update_with_media). So far the, this is my attempt at the extra final signing and authentication logic that is necessary. The formation of the Body, using Boundaries is done elsewhere, and I don't think is the stumbling block.
oauth_post({multipart, Boundary}, URL, Body, Consumer, Token, TokenSecret) ->
BodyH = base64:encode_to_string(crypto:hash(sha, Body)),
Signed = oauth:sign("POST", URL, [{"oauth_body_hash", BodyH}], Consumer, Token, TokenSecret),
{[[{ "oauth_signature", Sig}],
[{"oauth_body_hash", BBody}]], Rest} =
proplists:split(Signed, ["oauth_signature", "oauth_body_hash"]),
Encoded = [ {"oauth_signature", oauth:uri_encode(Sig)}
, {"oauth_body_hash", oauth:uri_encode(BBody)}
| Rest],
Sorted = lists:sort(Encoded),
Auth = lists:flatten(string:join([ K++"=\""++V++"\"" || {K,V} <- Sorted], ", ")),
OAuth = "OAuth " ++ Auth,
ContentType = "multipart/form-data;boundary=" ++ Boundary,
Headers = [ {"Authorization", OAuth}
, {"Content-Type", ContentType}
, {"Content-Length", integer_to_list(length(Body))}],
Request = {URL, Headers, ContentType, Body},
httpc:request(post, Request, [], []).
But so far this method call fails to Authenticate. Can anyone, with this domain expertise, see what I'm doing wrong? Many thanks.
After Answer
Per Paul's answer below, this is what I ended up using. Updated my fork of the library too.
oauth_post({multipart, Boundary}, URL, Body, Consumer, Token, TokenSecret) ->
BodyH = base64:encode_to_string(crypto:hash(sha, Body)),
Signed = oauth:sign("POST", URL, [{"oauth_body_hash", BodyH}]
, Consumer, Token, TokenSecret),
{AuthorizationParams, []} =
lists:partition(fun({K, _}) -> lists:prefix("oauth_", K) end, Signed),
Headers = [ oauth:header(AuthorizationParams)],
ContentType = "multipart/form-data;boundary=" ++ Boundary,
Request = {URL, Headers, ContentType, Body},
httpc:request(post, Request, [], []).

The issue is that your code only URI-encodes the signature and the body hash. All oauth_* parameters must be URI-encoded, and especially oauth_nonce which is a Base64 string in this OAuth library.
Nonce = base64:encode_to_string(crypto:rand_bytes(32)), % cf. ruby-oauth
As a side note:
Fetching the value for key "oauth_body_hash" does not make sense since you passed this value in the first place (it is in BodyH).
You don't need to sort the oauth_ parameters in the signature.
You don't need the Content-Type and Content-Length headers, inets' httpc will add them for you unless you pass headers_as_is option.
Simply do:
oauth_post({multipart, Boundary}, URL, Body, Consumer, Token, TokenSecret) ->
BodyH = base64:encode_to_string(crypto:hash(sha, Body)),
Signed = oauth:sign("POST", URL, [{"oauth_body_hash", BodyH}], Consumer, Token, TokenSecret),
% URI encode values returned by oauth:sign/6 for the Authorization header
Auth = lists:flatten(string:join([ K++"=\""++oauth:uri_encode(V)++"\"" || {K,V} <- Signed], ", ")),
OAuth = "OAuth " ++ Auth,
ContentType = "multipart/form-data;boundary=" ++ Boundary,
Headers = [ {"Authorization", OAuth} ],
Request = {URL, Headers, ContentType, Body},
httpc:request(post, Request, [], []).

Related

Why am i still unauthorized? HTTP Twitch 2AuthO request in Lua with coro-http

I am 100% sure that my client-id and client-secret are valid. I used it in my python code and it just worked fine
local http = require("coro-http")
local json = require("json")
local url = "https://id.twitch.tv/oauth2/token"
local client_id = "<>"
local client_secret = "<>"
local headers = {
["Content-Type"] = "application/x-www-form-urlencoded"
}
local body = "client_id=" .. client_id .. "&client_secret=" .. client_secret .. "&grant_type=client_credentials"
local response, w = http.request("POST", url, headers, body)
print(w)
local data = json.decode(w)
local access_token = data.access_token
local headers = {
["Client-ID"] = client_id,
["Authorization"] = "Bearer " .. access_token
}
local response, b = http.request("GET", "https://api.twitch.tv/helix/channels?broadcaster_id=141981764", headers)
print(b)
Getting token and then do a simple get request
I found this repository which is doing exactly what you're trying to.
From the code you provided and the one from the above repo, I would say #LMD comment is the way to go. You need to urlencode your body string.
Maybe querystring from luvit could be a good starting point.

Transform request data in Krakrnd with lua

Using Krakend as api gateway.
I have an endpoint configured in krakend.json:
"endpoint":"/call",
"extra_config":{
"github.com/devopsfaith/krakend-lua/proxy":{
"sources":[
"/function.lua"
],
"pre":"pre_backend(request.load())",
"live":true,
"allow_open_libs":true
}
},
"method":"POST",
"output_encoding":"json",
"headers_to_pass":[
"*"
],
"backend":[
{
"url_pattern":"/api/v1/get_client_id",
[...]]
},
The endopont "/api/v1/get_client_id" recives just a param:
{"user_mail_1":"test#test.es"}
I want, whith the lua script my endopoint "/call" recives:
{"email":"test#test.es"}
and transform on before send:
{"user_mail_1":"test#test.es"}
I tried with gsub, but use body() as "string" is no efficient.
function pre_backend( req )
print('--Backend response, pre-logic:');
local r = req;
r:params('test','test');
r:query('lovelyquery')
r:body('test','test');
lolcal v = r:body():gsub('email', 'user_mail_1')
...
Is a way to parse "req" as a table, dict or something i can transform data?
Is another way to transform REQUEST data?
EXAMPLE WORKING WITH GSUB:
function pre_backend( req )
print('--Backend response, pre-logic:');
print('--req');
print(req);
print(type(req));
local r = req;
print('--body');
print(type(r:body()));
print(r:body())
local body_transformed = r:body():gsub('email', 'user_mail_1');
print('--body_transformed');
print(body_transformed);
print(type(body_transformed));
end
Console output:
2022/02/11 09:59:52 DEBUG: [http-server-handler: no extra config]
--Backend response, pre-logic:
--req
userdata: 0xc0004f9b60
userdata
--body
string
{"email" : "test#test.es","test_field":"email"}
--body_transformed
{"user_mail_1" : "test#test.es","test_field":"user_mail_1"}
string
As we can see the gsub is not efficient becouse replace all strings.
If I can work with req as table, dict or something similar, I can replace dict key/value. ex: req['xxx] = 'xxx' or iterate req.keys
gsub stands for global substitution. It replaces all occurances of the pattern in the string.
If you just want to replace "email" infront of an email address simply use a pattern that takes this into account.
print((r:body():gsub('(")(email)("%s-:%s-"%w+#%w+%.%w+")', "%1user_mail_1%3")))
Alternatively if you knwo that you only want to replace the first occurance of email you can simply do this:
print((r:body():gsub("email", "user_mail_1", 1)))
The thrid parameter will stop gsub after the first replacement.

Can't get httpc basic auth example to work with https

This is my first time using erlang and I decided to try and write a wrapper for an API. Here's what I've got so far:-
-module(my_api_wrapper).
%% API exports
-export([auth/0]).
auth() ->
application:start(inets),
application:start(ssl),
AuthStr = base64:encode_to_string("username:password"),
Method = post,
URL = "https://api.endpoint.com/auth",
Header = [{"Authorization", "Basic " ++ AuthStr}],
Type = "application/json",
Body = "{\"grant_type\":\"client_credentials\"}",
HTTPOptions = [],
Options = [],
httpc:request(Method, {URL, Header, Type, Body}, HTTPOptions, Options).
When testing this at the shell I get an error:-
{error,{failed_connect,[{to_address,{"api.endpoint.com",
443}},
{inet,[inet],closed}]}}
I can't figure out what I'm doing wrong here! I'm running this version Erlang/OTP 19 [erts-8.0.2]. Any help appreciated.
For anyone who it might help - here's exactly what I changed to make the code in my original question work - thanks to Dogbert for his comment above.
-module(my_api_wrapper).
%% API exports
-export([auth/0]).
auth() ->
application:start(inets),
application:start(ssl),
AuthStr = base64:encode_to_string("username:password"),
Method = post,
URL = "https://api.endpoint.com/auth",
Header = [{"Authorization", "Basic " ++ AuthStr}],
Type = "application/json",
Body = "{\"grant_type\":\"client_credentials\"}",
% ADD SSL CONFIG BELOW!
HTTPOptions = [{ssl,[{versions, ['tlsv1.2']}]}],
Options = [],
httpc:request(Method, {URL, Header, Type, Body}, HTTPOptions, Options).

erlang google oauth2 protocol for google calling apis

Hello I am writing oauth 2 library to access google api's and my code is as follows
jwt_create() ->
{ok,PemBin} = file:read_file("your-key-file.pem"),
PemEntry = public_key:pem_decode(PemBin),
[A,B] = PemEntry,
io:format("A:: ~p ~n",[A]),
PrivateKey = public_key:pem_entry_decode(PemEntry),
JwtHeaderJson = encode_json(jwt_header()),
JwtClaimsetJson = encode_json(jwt_claimset()),
ComputeSignature = compute_signature(JwtHeaderJson, JwtClaimsetJson, PrivateKey),
Z=binary:replace(
binary:replace(<<JwtHeaderJson/binary, ".", JwtClaimsetJson/binary, ".", ComputeSignature/binary>>,
<<"+">>, <<"-">>, [global]),
<<"/">>, <<"_">>, [global]),
io:format("JWT:: ~p ~n",[Z]).
compute_signature(Header, ClaimSet,#'RSAPrivateKey'{publicExponent=Exponent
,modulus=Modulus
,privateExponent=PrivateExponent}) ->
base64:encode(crypto:sign(rsa, sha256, <<Header/binary, ".", ClaimSet/binary>>,
[Exponent, Modulus, PrivateExponent])).
encode_json(JWToken) ->
base64:encode(jsx:encode(JWToken)).
I am getting error as follows:
exception error: no function clause matching
public_key:pem_entry_decode([{'PrivateKeyInfo',<<48,130,4,191,2,1,0,48,13,6,9,42,134,
72,134,247,13,1,1,1,5,0,4,130,4,...>>,
not_encrypted},
{'Certificate',<<48,130,3,96,48,130,2,72,160,3,2,1,2,2,8,
79,59,244,35,60,15,3,155,48,...>>,
not_encrypted}]) (public_key.erl, line 123)
in function googleoauth:jwt_create/0 (src/googleoauth.erl, line 55)
Please help me in generating JWS and JWT for OAUTH 2 for accessing google apis
You are passing the wrong thing to public_key:pem_entry_decode/1:
This will resolve your problem:
PrivateKey = public_key:pem_entry_decode(A),
public_key:pem_entry_decode/1 takes a single pem_entry() but a PEM file can contain many entries, perhaps your code PemEntry = public_key:pem_decode(PemBin) should read PemEntries = public_key:pem_decode(PemBin) instead?
Also note the line before assumes 2 list entries, you might have meant this instead (not sure your intent here though)?
[A|B] = PemEntry,

OAuth authentication, invalid signature

I tired to port the request-oauth library (based on python-request) to Python 3 (with help of 2to3) but I have problems to validate a signature with StatusNet (same API as Twitter).
When I do a request to oauth/request_token, I have no problem but to oauth/access_token I have an error 401 Invalid signature. I don't understand why because it seems to me that what I sign is correct.
For example, with the python 2 code, cf hook.py and auth.py (original from the git repo), I get :
signing_key = '0de1456373dfc9349dd38a48e61fc844&136d6b9a597ee57d4338254812681acd',
signing_raw = 'POST&http%3A%2F%2Fstatus2.dotzero.me%2Fapi%2Foauth%2Faccess_token&oauth_consumer_key%3Dec3ad931b294b51a5ff595c732acb7a5%26oauth_nonce%3D33448267%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1332279747%26oauth_token%3D2131043f3516bcb730d391ed2033a880%26oauth_verifier%3D8816492%26oauth_version%3D1.0'
oauth_hook.token.key = '2131043f3516bcb730d391ed2033a880'
oauth_hook.token.secret = '136d6b9a597ee57d4338254812681acd'
request.data_and_params = {'oauth_version': '1.0', 'oauth_signature': 'xyjxH5QcfZXnG111L7qANZ+ahRI=',
'oauth_token': '2131043f3516bcb730d391ed2033a880', 'oauth_nonce': '33448267',
'oauth_timestamp': '1332279747', 'oauth_verifier': '8816492',
'oauth_consumer_key': 'ec3ad931b294b51a5ff595c732acb7a5',
'oauth_signature_method': 'HMAC-SHA1'}
and with my python 3 port, cf hook.py and auth.py, I get :
signing_key = '0de1456373dfc9349dd38a48e61fc844&136d6b9a597ee57d4338254812681acd',
signing_raw = 'POST&http%3A%2F%2Fstatus2.dotzero.me%2Fapi%2Foauth%2Faccess_token&oauth_consumer_key%3Dec3ad931b294b51a5ff595c732acb7a5%26oauth_nonce%3D52360702%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1332278837%26oauth_token%3D2131043f3516bcb730d391ed2033a880%26oauth_verifier%3D8816492%26oauth_verifier%3D8816492%26oauth_version%3D1.0'
oauth_hook.token.key = '2131043f3516bcb730d391ed2033a880'
oauth_hook.token.secret = '136d6b9a597ee57d4338254812681acd'
request.data_and_params = {'oauth_nonce': '52360702', 'oauth_timestamp': '1332278837',
'oauth_verifier': '8816492', 'oauth_consumer_key': 'ec3ad931b294b51a5ff595c732acb7a5',
'oauth_signature_method': 'HMAC-SHA1', 'oauth_version': '1.0',
'oauth_token': '2131043f3516bcb730d391ed2033a880',
'oauth_signature': 'BRsb11dk++405uaq5pRS+CMUzbo='}
Both looks good to me but the first one succeed and the second returns a 401 error, invalid signature.
In both cases, I get the token.key and token.secret as the result of :
OAuthHook.consumer_key = self.ckey
OAuthHook.consumer_secret = self.csecret
oauth_hook = OAuthHook()
client = requests.session(hooks={'pre_request': oauth_hook})
response = client.post('%soauth/request_token' % (self.url), {'oauth_callback': 'oob'})
# new oauth_hook with the request token
oauth_hook = OAuthHook(response[b'oauth_token'][0],response[b'oauth_token_secret'][0])
Them, I go to oauth/authorize?oauth_token=%s" % oauth_hook.token.key to get authorize the app and get a pincode. After that I can do the problematic request
...
response = client.post('%soauth/request_token' % (self.url), {'oauth_callback': 'oob'})
oauth_hook = OAuthHook(response[b'oauth_token'][0],response[b'oauth_token_secret'][0])
# get the pincode from %soauth/authorize?oauth_token=%s" % (self.url, oauth_hook.token.key)
oauth_hook.token.set_verifier(pincode)
client = requests.session(hooks={'pre_request': oauth_hook})
response = client.post("%soauth/access_token" % (self.url),
{'oauth_verifier': pincode})
The signature code from the auth.py file is
def sign(self, request, consumer, token):
"""Builds the base signature string."""
key, raw = self.signing_base(request, consumer, token)
hashed = hmac.new(key.encode(), raw.encode(), sha1)
# Calculate the digest base 64.
return binascii.b2a_base64(hashed.digest())[:-1]
Any idea why it doesn't work with the py3k code ?
Thank you
Found the answer ! There were two oauth_verifier in the POST request, leading to a wrong signature...
You may need to verify the Authorization header string in your request. Normally it would be of the format:
'Authorization' => 'OAuth
realm="",oauth_timestamp="1243392158",oauth_nonce="VsaPHb",oauth_consumer_key="xxxxxxxxxxxxxxxxxx",oauth_token="xxxxxx-xxxx-xxxxxxxxxxxxxx",oauth_version="1.0",oauth_signature_method="HMAC-SHA1",oauth_signature="xxxxxxxxxxxxxxxxxxxx"'
In the above header value, check that the "oauth_signature" is decoded properly. That is, it should not contain values like: %3D. You can use this tool to decode the string.
This has worked for me. Hope it helps someone.

Resources