diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95e8c7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +*.beam +oauth.app diff --git a/LICENSE b/LICENSE index bbedde4..d20282c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,23 +1,23 @@ -Copyright (c) 2007, 2008, 2009 JackNyfe, Inc. . +Copyright (c) 2008-2009 Jacknyfe, Inc., http://jacknyfe.net. All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. diff --git a/Makefile b/Makefile index 2ed91ff..699f800 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,39 @@ -PROJECTNAME=mcd -PROJECTVERSION=1.0 +YAWS_EBIN = /local/lib/yaws/ebin -INSTALLDIR=$(prefix)/$(LIBDIR)/$(PROJECTNAME)-$(PROJECTVERSION)/ebin -LIBDIR=$(shell erl -eval 'io:format("~s~n", [code:lib_dir()])' -s init stop -noshell) +ERLCFLAGS = -W2 -I/local/lib -all: - mkdir -p ebin - for srcfile in src/*.erl; do erlc -o ebin $$srcfile; done +EMODS_COMMON = \ + error_monad \ + oauth + +EMODS_CLIENT = \ + nonce \ + oauth_app \ + oauth_supervisor \ + oauthclient + +ESRCS_CLIENT = ${EMODS_CLIENT:%=%.erl} +ESRCS_COMMON = ${EMODS_COMMON:%=%.erl} +EOBJS_CLIENT = ${ESRCS_CLIENT:.erl=.beam} +EOBJS_COMMON = ${ESRCS_COMMON:.erl=.beam} + +EOBJS = ${EOBJS_COMMON} ${EOBJS_CLIENT} + +ALL_OBJS = oauth.app ${EOBJS} test.beam + +all: ${ALL_OBJS} + +HARDWIRE_MODULES = perl -pe 's@MODULES@join(", ", split(/\s+/, $$ENV{MODULES}))@e;' + +oauth.app: oauth.app.in + MODULES="${EMODS_COMMON} ${EMODS_CLIENT}" \ + ${HARDWIRE_MODULES} \ + < oauth.app.in > oauth.app clean: - rm -rf ebin + rm -f ${ALL_OBJS} + +.SUFFIXES: .erl .beam -install: - mkdir -p $(INSTALLDIR) - for f in ebin/*.beam; do install $$f $(INSTALLDIR); done +.erl.beam: + erlc $(ERLCFLAGS) -o . $< diff --git a/README b/README index e69de29..052af69 100644 --- a/README +++ b/README @@ -0,0 +1,7 @@ +Erlang implementation of OAuth client by JS-Kit + +Copyright (c) 2008-2009 Jacknyfe, Inc. All rights reserved. + +This client is interoperable with OAuth 1.0 and 1.0a servers. + +For documentation and samples please check out oauthclient.erl and test.erl. diff --git a/TODO b/TODO new file mode 100644 index 0000000..aa4e659 --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ +S: support RSA-SHA1 +S: WWW-Authenticate when needed +S: handle Authorization: header +S: when signature check fails, provide more diagnostics. +S: test with SSL +S: the supervisor modules should be a callback as well +C: google claims "no oauth parameters" when making a 'post' request with + get_request_token. diff --git a/error_monad.erl b/error_monad.erl new file mode 100644 index 0000000..042626b --- /dev/null +++ b/error_monad.erl @@ -0,0 +1,51 @@ +% +% Copyright (c) 2008-2009 Jacknyfe, Inc., http://jacknyfe.net. +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright notice, +% this list of conditions and the following disclaimer in the documentation +% and/or other materials provided with the distribution. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% + +-module(error_monad). +-export([ + do/1, + do/2, + error/1, + error/2, + error/3 +]). + +% Poor man's Error monad. +do(F) -> do(F, undefined). + +do([], Arg) -> Arg; + +do([Fun | RestFuns], Arg) -> + case Fun(Arg) of + {error, Error} -> Error; + V -> do(RestFuns, V) + end. + +% "Fail" functions. The application will see {error, A, B, ...} value extracted +% out of the monad. +error(A) -> {error, {error, A}}. +error(A, B) -> {error, {error, A, B}}. +error(A, B, C) -> {error, {error, A, B, C}}. diff --git a/nonce.erl b/nonce.erl new file mode 100644 index 0000000..a48f997 --- /dev/null +++ b/nonce.erl @@ -0,0 +1,106 @@ +% +% Nonce server +% +% Copyright (c) 2008-2009 Jacknyfe, Inc., http://jacknyfe.net. +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright notice, +% this list of conditions and the following disclaimer in the documentation +% and/or other materials provided with the distribution. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% + +-module(nonce). +-behaviour(gen_server). + +-export([ + % public API + get_ts_nonce/1, + start_link/0, + + % gen_server callbacks + code_change/3, + handle_call/3, + handle_cast/2, + handle_info/2, + init/1, + terminate/2 +]). + +-define(RESET_PERIOD, 5000). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Public API +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +get_ts_nonce(ServerRef) -> + gen_server:call(ServerRef, {get_ts_nonce}). + + +start_link() -> + gen_server:start_link({local, oauth_nonce}, ?MODULE, args, []). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% gen_server callbacks +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +handle_call({get_ts_nonce}, _From, {_LastTs, N}) -> + Ts = get_ts(), + {reply, {Ts, "jskitnonce" ++ integer_to_list(N)}, {Ts, N + 1}}; + +handle_call(_Request, _From, State) -> + {noreply, State}. + + +handle_cast(_Request, State) -> + {noreply, State}. + + +handle_info({reset}, {LastTs, _N} = State) -> + Ts = get_ts(), + if + % We saw last request this very second, so it's not yet safe to reset + % Nonce value (otherwise there's a chance it won't be unique). + Ts == LastTs -> {noreply, State}; + + true -> {noreply, {LastTs, 0}} + end. + + +init(_Args) -> + timer:send_interval(?RESET_PERIOD, {reset}), + {ok, {0, 0}}. + + +terminate(_Reason, _State) -> + ok. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Internal functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +get_ts() -> + {MegaSecs, Secs, _} = now(), + MegaSecs * 1000000 + Secs. diff --git a/oauth.app.in b/oauth.app.in new file mode 100644 index 0000000..f4a21af --- /dev/null +++ b/oauth.app.in @@ -0,0 +1,9 @@ +{application, oauth, + [ {description, "OAuth implementation"}, + {vsn, "0.1"}, + {modules, [ MODULES ] }, + {registered, [oauth_nonce]}, + {applications, [kernel, stdlib, sasl]}, + {mod, {oauth_app, []}} + ] +}. diff --git a/oauth.erl b/oauth.erl new file mode 100644 index 0000000..5c075c5 --- /dev/null +++ b/oauth.erl @@ -0,0 +1,260 @@ +% +% Copyright (c) 2008-2009 Jacknyfe, Inc., http://jacknyfe.net. +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright notice, +% this list of conditions and the following disclaimer in the documentation +% and/or other materials provided with the distribution. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% + +-module(oauth). +-export([ + decode_parameters/1, + encode_parameters/1, + ensure_parameters_present/4, + generate_signature/2, + make_signature_base_string/3, + percent_decode/1, + percent_encode/1 +]). + +decode_parameters(URLEncodedString) -> + [X || + X <- [ + case string:tokens(KV, "=") of + [K, V] -> {percent_decode(K), percent_decode(V)}; + [K] -> {percent_decode(K), ""}; + _ -> invalid + end + || KV <- string:tokens(URLEncodedString, "&") + ], + X =/= invalid + ]. + +%% + +encode_parameters(Params) -> + { + "application/x-www-urlencoded", + string:join( + [percent_encode(K) ++ "=" ++ percent_encode(V) || {K,V} <- Params], "&" + ) + }. + +%% + +generate_signature(plaintext, KVPairs) -> + error_monad:do( + get_required_params_ErrorMonadFuns( + KVPairs, [consumer_secret, token_secret] + ) + ++ + [ + fun({_Params, [ConsumerSecret, TokenSecret]}) -> + {ok, "PLAINTEXT", + percent_encode(generate_signature_key(ConsumerSecret, TokenSecret)), + [] + } + end + ], + nothing + ); + +generate_signature(hmac_sha1, KVPairs) -> + generate_complex_signature("HMAC-SHA1", KVPairs, + { + [consumer_secret, token_secret], + + fun([ConsumerSecret, TokenSecret]) -> + generate_signature_key(ConsumerSecret, TokenSecret) + end + }, + + fun(BaseString, Key) -> + crypto:sha_mac(Key, BaseString) + end + ); + +generate_signature(rsa_sha1, KVPairs) -> + generate_complex_signature("RSA-SHA1", KVPairs, + { + [private_key], + fun([PrivateKey]) -> PrivateKey end + }, + + fun(BaseString, Key) -> + public_key:sign(list_to_binary(BaseString), Key) + end + ). + +%% + +percent_decode(X) -> percent_decode(X, []). + +percent_decode([], Acc) -> lists:reverse(Acc); + +percent_decode([$%, H, L | T], Acc) -> + case hex_value(H) of + invalid -> percent_decode([H, L | T], [$% | Acc]); + {ok, HV} -> + case hex_value(L) of + invalid -> percent_decode([L | T], [H, $%, Acc]); + {ok, LV} -> percent_decode(T, [HV * 16 + LV | Acc]) + end + end; + +percent_decode([H | T], Acc) -> percent_decode(T, [H | Acc]). + +%% + +% +% Encoding per +% http://oauth.net/core/1.0/#encoding_parameters +% +percent_encode(X) -> lists:flatten(percent_encode_deep(X)). + +percent_encode_deep([]) -> []; +percent_encode_deep([X | Rest]) -> + [char_percent_encoding(X) | percent_encode_deep(Rest)]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Internal functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +char_percent_encoding(X) when (X >= $0) andalso (X =< $9) -> X; +char_percent_encoding(X) when (X >= $a) andalso (X =< $z) -> X; +char_percent_encoding(X) when (X >= $A) andalso (X =< $Z) -> X; +char_percent_encoding($_ = X) -> X; +char_percent_encoding($. = X) -> X; +char_percent_encoding($- = X) -> X; +char_percent_encoding($~ = X) -> X; +char_percent_encoding(X) -> io_lib:format("%~.16B", [X]). + +%% + +hex_value(X) when ((X >= $0) andalso (X =< $9)) -> {ok, X - $0}; +hex_value(X) when ((X >= $a) andalso (X =< $f)) -> {ok, X - $a + 10}; +hex_value(X) when ((X >= $A) andalso (X =< $F)) -> {ok, X - $A + 10}; +hex_value(_) -> invalid. + +%% + +ensure_parameters_present(Params, Keys, Extra, Accessor) -> + Result = error_monad:do( + [ + fun(Values) -> + case Accessor(Params, Key) of + {ok, Value} -> Values ++ [Value]; + Error -> {error, Error} + end + end + || Key <- Keys + ], + [] + ), + + case Result of + {error, _} = Error -> Error; + _ -> {Params, Result, Extra} + end. + +%% + +generate_complex_signature(SignMethod, KVPairs, {KeyGenParams, KeyGenFun}, SignatureGenFun) -> + error_monad:do( + get_required_params_ErrorMonadFuns( + KVPairs, [endpoint, params] ++ KeyGenParams + ) + ++ [ + fun({_KVPairs, [EndPoint, Params | KGParams]}) -> + BaseString = make_signature_base_string(SignMethod, Params, EndPoint), + + Signature = SignatureGenFun(BaseString, KeyGenFun(KGParams)), + + + {ok, SignMethod, base64:encode_to_string(Signature), [ + {endpoint, EndPoint}, + {basestring, BaseString} + ]} + end + ], + nothing + ). + +%% + +generate_signature_key(ConsumerSecret, TokenSecret) -> + percent_encode(ConsumerSecret) ++ "&" ++ percent_encode(TokenSecret). + +%% + +get_required_params_ErrorMonadFuns(KVPairs, Keys) -> + [ + fun(_) -> + ensure_parameters_present(KVPairs, Keys, unused, + fun(P, K) -> + case proplists:get_value(K, P) of + undefined -> {error, {error, missing_parameter, K, []}}; + Value -> {ok, Value} + end + end + ) + end, + + fun({PassedKVPairs, Values, unused}) -> {PassedKVPairs, Values} end + ]. + +%% + +make_signature_base_string(SignMethod, Params, EndPoint) -> + + {HttpMethod, Scheme, Authority, Port, Path} = EndPoint, + + Sorted = lists:sort( + fun({A_k, A_v}, {B_k, B_v}) -> + if + A_k < B_k -> true; + A_k > B_k -> false; + true -> A_v < B_v + end + end, + [ + {percent_encode(K), percent_encode(V)} + || {K, V} <- [{"oauth_signature_method", SignMethod} | Params] + ] + ), + + % XXX: reuse smth + ParString = string:join([K ++ "=" ++ V || {K, V} <- Sorted], "&"), + + SchemeLC = string:to_lower(Scheme), + URL = SchemeLC ++ "://" ++ string:to_lower(Authority) ++ + (if + ((SchemeLC == "http") andalso (Port == 80)) -> ""; + ((SchemeLC == "https") andalso (Port == 443)) -> ""; + true -> ":" ++ integer_to_list(Port) + end) + ++ Path, + + percent_encode(string:to_upper(HttpMethod)) ++ + "&" ++ + percent_encode(URL) ++ + "&" ++ + percent_encode(ParString). diff --git a/oauth_app.erl b/oauth_app.erl new file mode 100644 index 0000000..5e3e73b --- /dev/null +++ b/oauth_app.erl @@ -0,0 +1,36 @@ +% +% Copyright (c) 2008-2009 Jacknyfe, Inc., http://jacknyfe.net. +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright notice, +% this list of conditions and the following disclaimer in the documentation +% and/or other materials provided with the distribution. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% + +-module(oauth_app). +-behavior(application). +-export([start/0, start/2, stop/1]). + +stop(_) -> ok. + +start() -> application:start(?MODULE). + +start(_Type, _Args) -> + oauth_supervisor:start_link(). diff --git a/oauth_supervisor.erl b/oauth_supervisor.erl new file mode 100644 index 0000000..ba76eba --- /dev/null +++ b/oauth_supervisor.erl @@ -0,0 +1,38 @@ +% +% Copyright (c) 2008-2009 Jacknyfe, Inc., http://jacknyfe.net. +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright notice, +% this list of conditions and the following disclaimer in the documentation +% and/or other materials provided with the distribution. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% + +-module(oauth_supervisor). +-behaviour(supervisor). +-export([start_link/0, init/1]). + +start_link() -> + supervisor:start_link(?MODULE, none). + +init(none) -> + {ok, {{one_for_one, 10, 10}, + [{nonce, {nonce, start_link, []}, + permanent, 10000, worker, [nonce]} + ]}}. diff --git a/oauthclient.erl b/oauthclient.erl new file mode 100644 index 0000000..533bf31 --- /dev/null +++ b/oauthclient.erl @@ -0,0 +1,751 @@ +% +% OAuth client +% +% +% Copyright (c) 2008-2009 Jacknyfe, Inc., http://jacknyfe.net. +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright notice, +% this list of conditions and the following disclaimer in the documentation +% and/or other materials provided with the distribution. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% + +-module(oauthclient). + +% +% Public API: +% +% Generally the workflow is: +% * new +% * get_request_token +% * mk_authorization_url +% * authorization_completed +% * get_access_token +% * mk_access_request +% +% ====================================================================== +% +% authorization_completed(State, Verifier) -> {NewState, ok} +% +% Make the client aware that the user has authorized the request token. +% Verifier is the verification code passed via the callback or "out-of-band" +% measures (e.g. the user typed that in). +% +% ====================================================================== +% +% authorization_failed(State) -> {NewState, ok} +% +% Make the client aware that the user has denied authorization of the request +% token. After calling this function it's ok to start over again, obtaining +% another request token. +% +% ====================================================================== +% +% dump_state(State) -> StateDump +% +% This function dumps state bits that are *internal*, i.e. do not belong to +% the configuration passed to new/*. The output is a tuple (including the +% format version number) that can be passed to reinstantiate/2 to re-create the +% current state. +% +% ====================================================================== +% +% get_access_token(State) -> +% {NewState, {ok, Params}} | {NewState, {error, Reason}} +% +% get_access_token(State, ExtraParams) -> +% {NewState, {ok, Params}} | {NewState, {error, Reason}} +% +% Exchange an authorized request token for an access token, optionally passing +% additional parameters ExtraParams. Returns key-value pairs (Params) returned +% by the server. +% +% ====================================================================== +% +% get_opaque(State) -> term +% +% Return the opaque value contained in the state. +% +% ====================================================================== +% +% get_request_token(State, Params) -> +% {NewState, {ok, RequestToken}} | {NewState, {error, Reason}} +% +% Obtains a request token from the server. Params is a proplist of additional +% parameters in the HTTP request (both keys and values are strings). +% +% ====================================================================== +% +% mk_access_request(State, HttpMethod, AuthParamsLocation, BaseURL, Params) -> +% {NewState, {ok, {URL, Headers, ContentType, Body}}} +% +% HttpMethod = delete | get | post | put +% AuthParamsLocation = {authorization, Realm} | with_rest +% Realm = @string | none +% +% Construct a request for a protected resource located at BaseURL, using given +% HttpMethod. Authentication parameters are placed either into "Authorization" +% HTTP header (with Realm, if needed) or along with the rest of parameters +% (e.g. into the URL or request body). +% +% ====================================================================== +% +% mk_authorization_url(State) -> {NewState, {ok, URL}} +% +% Returns a URL that the user has to visit to authorize the request token. +% +% ====================================================================== +% +% mk_signed_request(SignDetails, HttpMethod, URL, AuthParamsLocation, Params) -> +% {URL, Headers, ContentType, Body} +% +% SignDetails = {ConsumerKey, ConsumerSecret, Nonce, SignMethod} +% +% XXX: This does not really belong to OAuth. This is basically a function to +% make a request signed as per OAuth specification. +% +% See also: +% * http://niallohiggins.com/2009/03/13/opensocial-and-2-legged-oauth/ +% +% ====================================================================== +% +% new(Args) -> State +% +% Initializes OAuth client according to Args and returns its state. +% +% XXX: Args needs to be documented. Check out test.erl for examples. +% +% ====================================================================== +% +% new(Args, InitialState, Params) +% +% Do not use unless you know what you are doing. +% +% ====================================================================== +% +% reinstantiate(Args, StateDump) -> NewState +% +% Reinstantiate OAuth client state from the StateDump. Args is the same as in +% call to new/*. +% +% ====================================================================== +% +% set_opaque(State, Opaque) -> NewState +% +% Set opaque value in the state to Opaque. +% + +-export([ + authorization_completed/2, + authorization_failed/1, + dump_state/1, + get_access_token/1, + get_access_token/2, + get_opaque/1, + get_request_token/2, + mk_access_request/5, + mk_authorization_url/1, + mk_signed_request/5, + new/1, + new/3, + reinstantiate/2, + set_opaque/2 +]). + +-record(state, { + access_token_api, + access_token_renewal_allowed, + authorization_url, + callback_url, + consumer_key, + consumer_secret, + debug_output, + nonce_server, + opaque, + private_key, + request_token_api, + server_version, + signature_method, + state = initial, + token, + token_secret, + verifier +}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Public API +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +authorization_completed(State, Verifier) -> + #state{ state = have_request_token } = State, + + { + State#state{ + state = have_authorized_token, + verifier = Verifier + }, + ok + }. + +%% + +authorization_failed(State) -> + State#state{ state = initial }. + +%% + +dump_state(State) -> + { + 1, % format version number + State#state.opaque, + State#state.state, + State#state.server_version, + State#state.token, + State#state.token_secret, + State#state.verifier + }. + + +%% + +get_access_token(State) -> get_access_token(State, []). + +% +% OAuth Core specification v.1.0 explicitly forbids[1] passing application +% parameters in "get access token" request. However, some providers adhere to +% OAuth extensions[2] and thus warrant an option to bypass this restriction. +% +% [1]: http://oauth.net/core/1.0#auth_step3 +% [2]: http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html +% +get_access_token(#state{ state = CurState } = State, AppParams) -> + AllowedStates = [have_authorized_token] ++ + case State#state.access_token_renewal_allowed of + true -> [have_access_token]; + false -> [] + end, + + case lists:member(CurState, AllowedStates) of + true -> ok; + false -> + erlang:error({get_access_token, + {invalid_state, CurState}, + {allowed_states, AllowedStates} + }) + end, + + {Method, URL} = State#state.access_token_api, + + {ContentType, Body, _, _} = prepare_request_params( + State, + + [{"oauth_token", State#state.token}] + ++ + case State#state.server_version of + '1.0' -> []; + '1.0a' -> [{"oauth_verifier", State#state.verifier}] + end, + + AppParams, + + [ + create_endpoint_param(Method, URL), + {token_secret, State#state.token_secret} + ] + ), + + Headers = [], + + handle_response( + State, + + http_request(State, Method, URL, ContentType, Body, Headers), + + CurState, + + [ + fun({Params, Token, TokenSecret}) -> + { + set_state(State, have_access_token, [ + {token, Token}, + {token_secret, TokenSecret} + ]), + + {ok, Params} + } + end + ] + ). + +%% + +get_opaque(State) -> State#state.opaque. + +%% + +get_request_token(State, AppParams) -> + #state{ state = initial } = State, + + {Method, URL} = State#state.request_token_api, + + {"application/x-www-urlencoded" = ContentType, Body, _, _} = + prepare_request_params( + State, + [ + % OAuth 1.0a requires the callback to be present when obtaining a + % request token. + {"oauth_callback", State#state.callback_url} + ], + AppParams, + [ + create_endpoint_param(Method, URL), + {token_secret, ""} + ] + ), + + Headers = [], + + handle_response( + State, + + http_request(State, Method, URL, ContentType, Body, Headers), + + initial, + + [ + fun({Params, OAuthToken, OAuthTokenSecret}) -> + % + % This parameter is only returned by 1.0a servers + % See section 6.1.2 of http://oauth.googlecode.com/svn/spec/core/1.0a/drafts/3/oauth-core-1_0a.html + % + ServerVersion = + case proplists:get_value("oauth_callback_confirmed", Params) of + "true" -> '1.0a'; + _ -> '1.0' + end, + + { + State#state{ + server_version = ServerVersion, + state = have_request_token, + token = OAuthToken, + token_secret = OAuthTokenSecret + }, + + {ok, OAuthToken} + } + end + ] + ). + +%% + +mk_access_request(State, HttpMethod, AuthParamsLocation, URL, Params) -> + #state{ state = have_access_token } = State, + + {"application/x-www-urlencoded" = ContentType, AllParams, AppParams, + AuthParams} = prepare_request_params( + State, + + % XXX: when this function is called by mk_signed_request (which is a + % hack), the token is undefined. + [ {K, V} + || {K, V} <- [{"oauth_token", State#state.token}], + V =/= undefined + ], + + Params, + [ + create_endpoint_param(HttpMethod, URL), + {token_secret, State#state.token_secret} + ] + ), + + {Headers, OutParams} = + case AuthParamsLocation of + {authorization, Realm} -> + { + [{"authorization", + "OAuth " ++ + case Realm of + none -> ""; + _ -> "realm=\"" ++ Realm ++ "\", " + end ++ string:join( + [ oauth:percent_encode(K) ++ "=\"" ++ oauth:percent_encode(V) ++ + "\"" || {K, V} <- AuthParams + ], + ", " + ) + }], + AppParams + }; + + with_rest -> {[], AllParams} + end, + + CompleteURL = case OutParams of + [] -> URL; + _ -> URL ++ "?" ++ OutParams + end, + + SimpleResp = {CompleteURL, Headers, unknown, none}, + + { + State, + { + ok, + case HttpMethod of + delete -> SimpleResp; + get -> SimpleResp; + post -> {URL, Headers, ContentType, OutParams}; + put -> SimpleResp + end + } + }. + + +%% + +mk_authorization_url(State) -> + #state{ state = have_request_token } = State, + + {URL, Options, Params} = State#state.authorization_url, + + CompleteParams = Params ++ + case lists:member(token_optional, Options) of + true -> []; + false -> [{"oauth_token", State#state.token}] + end + ++ + case State#state.server_version of + '1.0a' -> []; + '1.0' -> + case State#state.callback_url of + % We are talking to a 1.0 server, which doesn't require us to + % provide a callback URL. However our API requires the application to + % supply us with one; exceptions are denoted as "oob" (out of band) + % per OAuth 1.0a spec. + % + % So if we are talking to a 1.0 server AND the callback url is + % specified as "oob", we can just omit it. It won't break the + % protocol. + "oob" -> []; + CB -> [{"oauth_callback", CB}] + end; + + Unexpected -> + erlang:error({unexpected_server_version, Unexpected}) + end, + + { + State, + { + ok, + URL ++ + case CompleteParams of + [] -> ""; + _ -> + {"application/x-www-urlencoded", Encoded} = + oauth:encode_parameters(CompleteParams), + "?" ++ Encoded + end + } + }. + +%% + +mk_signed_request(SignDetails, HttpMethod, URL, AuthParamsLocation, Params) -> + + {ConsumerKey, ConsumerSecret, Nonce, SignMethod} = SignDetails, + + {_NewState, Result} = + mk_access_request( + #state { + consumer_key = ConsumerKey, + consumer_secret = ConsumerSecret, + nonce_server = Nonce, + signature_method = SignMethod, + state = have_access_token, + token_secret = "" % so that generate_signature doesn't complain + }, + HttpMethod, AuthParamsLocation, URL, Params), + + Result. + + +%% + +new(Args) -> + AuthorizationURL = get_mandatory_value(Args, authorization_url), + + CallbackURL = get_mandatory_value(Args, callback_url), + + SignatureMethod = get_mandatory_value(Args, signature_method), + PrivateKey = case SignatureMethod of + rsa_sha1 -> get_mandatory_value(Args, private_key); + _ -> not_needed + end, + + #state{ + access_token_api = get_mandatory_value(Args, access_token_api), + access_token_renewal_allowed = + proplists:get_value(access_token_renewal_allowed, Args, false), + authorization_url = AuthorizationURL, + callback_url = CallbackURL, + consumer_key = get_mandatory_value(Args, consumer_key), + consumer_secret = get_mandatory_value(Args, consumer_secret), + debug_output = proplists:get_value(debug_output, Args, []), + nonce_server = get_mandatory_value(Args, nonce_server), + private_key = PrivateKey, + request_token_api = get_mandatory_value(Args, request_token_api), + signature_method = SignatureMethod + }. + + +% +% Create a client in a specific state InitialState. Args is the same as in the +% call to new/1. +new(Args, InitialState, Params) -> + set_state(new(Args), InitialState, Params). + +%% + +set_opaque(State, Opaque) -> + State#state{ opaque = Opaque }. + +%% + +reinstantiate(Args, StateDump) -> + update_state(new(Args), StateDump). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Internal functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +get_mandatory_value(KVs, Key) -> + case proplists:get_value(Key, KVs) of + undefined -> + error_logger:error_msg("~s: Mandatory parameter '~s' is undefined~n", + [?MODULE, Key] + ), + erlang:error({undefined_param, Key}); + + Value -> Value + end. + +%% + +get_mandatory_values(KVs, Keys) -> + [get_mandatory_value(KVs, K) || K <- Keys]. + +%% + +prepare_request_params(StateData, ExtraOAuthParams, AppParams, SignData) -> + {Ts, N} = nonce:get_ts_nonce(StateData#state.nonce_server), + + LTs = integer_to_list(Ts), + OAuthParams = ExtraOAuthParams ++ [ + {"oauth_consumer_key", StateData#state.consumer_key}, + {"oauth_timestamp", LTs}, + {"oauth_nonce", LTs ++ N}, + {"oauth_version", "1.0"} + ], + + {ok, SignMethod, Signature, _DebugInfo} = oauth:generate_signature( + StateData#state.signature_method, + [ + {consumer_secret, StateData#state.consumer_secret}, + {params, OAuthParams ++ AppParams}, + {private_key, StateData#state.private_key} + ] ++ SignData + ), + + SignatureParams = [ + {"oauth_signature_method", SignMethod}, + {"oauth_signature", Signature} + ], + + ContentType = "application/x-www-urlencoded", + + {ContentType, AllEncodedParams} = oauth:encode_parameters( + AppParams ++ OAuthParams ++ SignatureParams + ), + + {ContentType, AppEncodedParams} = oauth:encode_parameters(AppParams), + + { + ContentType, + AllEncodedParams, + AppEncodedParams, + OAuthParams ++ SignatureParams + }. + +%% + +handle_response(StateData, Response, ErrorState, ErrorMonadFuns) -> + + ReportError = fun(Details) -> + {error, + { + StateData#state{ state = ErrorState }, + {error, Details} + } + } + end, + + error_monad:do( + [ + fun(R) -> + case R of + % The default is to return 'full result': see http(3), search for + % 'full_result'. + {ok, Result} -> Result; + {error, _} = Error -> ReportError(Error) + end + end, + + fun({StatusLine, _Headers, Body} = R) -> + case StatusLine of + {"HTTP/1.1", 200, "OK"} -> oauth:decode_parameters(Body); + _ -> ReportError({invalid_http_response, R}) + end + end, + + fun(Params) -> + case proplists:get_value("oauth_token", Params) of + undefined -> ReportError({missing_response_parameter, oauth_token}); + OAuthToken -> {Params, OAuthToken} + end + end, + + fun({Params, OAuthToken}) -> + case proplists:get_value("oauth_token_secret", Params) of + undefined -> + ReportError({missing_response_parameter, oauth_token_secret}); + + OAuthTokenSecret -> {Params, OAuthToken, OAuthTokenSecret} + end + end + ] ++ ErrorMonadFuns, + Response + ). + +%% + +create_endpoint_param(HttpMethod, URL) -> + {Scheme, _Credentials, Authority, Port, Path, _Query} = http_uri:parse(URL), + + {endpoint, + {atom_to_list(HttpMethod), atom_to_list(Scheme), Authority, Port, Path}}. + +%% + +http_request(State, Method, URL, ContentType, Body, Headers) -> + Request = case Method of + get -> {URL ++ "?" ++ Body, Headers}; + post -> {URL, Headers, ContentType, Body} + end, + + case lists:member(http_requests, State#state.debug_output) of + true -> + io:format("~nOAuth client HTTP Request:~n~s ~p~n", [Method, Request]); + _ -> ok + end, + + http:request(Method, Request, [], []). + +%% + +set_state(State, have_access_token, Params) -> + set_state_and_tokens(State, have_access_token, Params); + +set_state(State, have_request_token, Params) -> + set_state_and_tokens(State, have_request_token, Params). + +%% + +set_state_and_tokens(State, NewState, Params) -> + [Token, TokenSecret] = get_mandatory_values(Params, [token, token_secret]), + + State#state{ + state = NewState, + token = Token, + token_secret = TokenSecret + }. + +%% + +update_state(State, + { + 0, % format version number + Opaque, + CurState, + Token, + TokenSecret + } +) -> update_state(State, + CurState, + Opaque, + '1.0', + Token, + TokenSecret, + undefined); + +update_state(State, + { + 1, + Opaque, + CurState, + ServerVersion, + Token, + TokenSecret, + Verifier + } +) -> update_state(State, + CurState, + Opaque, + ServerVersion, + Token, + TokenSecret, + Verifier); + +update_state(_State, Dump) -> + erlang:error({unknown_state_dump_format, Dump}). + +% This function fills all required fields, so whatever dump format we +% encounter, we have to use this one to make sure all required fields are +% filled. +update_state(State, + CurState, + Opaque, + ServerVersion, + Token, + TokenSecret, + Verifier +) -> + State#state + { + opaque = Opaque, + server_version = ServerVersion, + state = CurState, + token = Token, + token_secret = TokenSecret, + verifier = Verifier + }. diff --git a/test.erl b/test.erl new file mode 100644 index 0000000..cd71cdc --- /dev/null +++ b/test.erl @@ -0,0 +1,200 @@ +% +% Copyright (c) 2008-2009 Jacknyfe, Inc., http://jacknyfe.net. +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright notice, +% this list of conditions and the following disclaimer in the documentation +% and/or other materials provided with the distribution. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% + +-module(test). +-compile(export_all). + +run() -> + % + % XXX: Unit testing, ugly. :-) Should be somewhere else. + % + + {error, missing_parameter, consumer_secret, _} = + oauth:generate_signature(hmac_sha1, []), + + {ok, "HMAC-SHA1", "tR3+Ty81lMeYAr/Fid0kMTYa/WM=", _} = + oauth:generate_signature(hmac_sha1, [ + {consumer_secret, "kd94hf93k423kf44"}, + {endpoint, {"get", "http", "photos.example.net", 80, "/photos"}}, + {params, [ + {"oauth_consumer_key", "dpf43f3p2l4k3l03"}, + {"oauth_token", "nnch734d00sl2jdk"}, + {"oauth_timestamp", "1191242096"}, + {"oauth_nonce", "kllo9940pd9333jh"}, + {"oauth_version", "1.0"}, + {"file", "vacation.jpg"}, + {"size", "original"} + ]}, + {token_secret, "pfkkdhi9sl3r4s00"} + ]). + +%% + +signed_request() -> + [application:start(X) || X <- [crypto, inets, ssl]], + {ok, Nonce} = nonce:start_link(), + + SignParams = {"consumer key", "consumer secret", Nonce, hmac_sha1}, + + [ + oauthclient:mk_signed_request(SignParams, HttpMethod, "http://js-kit.com", + AuthParamsLocation, [{"a", "b"}]) + || + {HttpMethod, AuthParamsLocation} <- [ + {get, with_rest}, + {put, {authorization, "JS-Kit"}}, + {post, {authorization, "JS-Kit"}}, + {post, with_rest} + ] + ]. + +%% + +termie_make_request(State, AccessURL, Params) -> + {_C4, {ok, {URL, Headers, _ContentType, _Body}}} = + oauthclient:mk_access_request(State, get, with_rest, AccessURL, Params), + + http:request(get, {URL, Headers}, [], []). + +%% + +termie_config(SignatureMethod) -> + BaseURL = "http://term.ie/oauth/example", + + [ + {access_token_api, {get, BaseURL ++ "/access_token.php"}}, + {authorization_url, + {BaseURL ++ "/not_implemented", [], []}}, + {callback_url, "oob"}, + {nonce_server, oauth_nonce}, + {consumer_key, "key"}, + {consumer_secret, "secret"}, + {request_token_api, {get, BaseURL ++ "/request_token.php"}}, + {signature_method, SignatureMethod} + ]. + +%% + +termie() -> + [application:start(X) || X <- [crypto, inets, ssl]], + + {ok, _} = nonce:start_link(), + + AccessURL = "http://term.ie/oauth/example/echo_api.php", + + [ + begin + C = oauthclient:new(termie_config(SignatureMethod)), + + {C2, Result2} = oauthclient:get_request_token(C, []), + case Result2 of + {ok, _} -> ok; + {error, Error} -> io:format("get_request_token: ~p~n", [Error]) + end, + + {C21, ok} = oauthclient:authorization_completed(C2, + "termie is oauth 1.0 server, so it doesn't need verification codes" + ), + + {C3, {ok, _}} = oauthclient:get_access_token(C21), + + termie_make_request(C3, AccessURL, + [{"method", atom_to_list(SignatureMethod)}]) + end + || SignatureMethod <- [hmac_sha1, plaintext] + ]. + +%% + +google() -> + [application:start(X) || X <- [crypto, inets, ssl]], + + {ok, Nonce} = nonce:start_link(), + + BaseURL = "https://www.google.com/accounts/OAuth", + + ProviderSettings = [ + {access_token_api, {get, BaseURL ++ "GetAccessToken"}}, + {authorization_url, {BaseURL ++ "AuthorizeToken", [], []}}, + {callback_url, "oob"}, + {debug_output, [http_requests]}, + {nonce_server, Nonce}, + {consumer_key, "google consumer key"}, + {consumer_secret, "google consumer secret"}, + {request_token_api, {get, BaseURL ++ "GetRequestToken"}}, + {signature_method, hmac_sha1} + ], + + thread_state( + oauthclient:new(ProviderSettings), + fun(C) -> + oauthclient:reinstantiate(ProviderSettings, oauthclient:dump_state(C)) + end, + [ + fun(C) -> + {C2, Result2} = oauthclient:get_request_token(C, [ + {"scope", "http://www.blogger.com/feeds/"} + ]), + + case Result2 of + {ok, _} -> io:format("got request token~n"); + {error, Error} -> io:format("get_request_token: ~p~n", [Error]); + Unexpected -> + erlang:error({unexpected_return, get_request_token, Unexpected}) + end, + + {ok, C2} + end, + + fun(C) -> + {C2, {ok, URL}} = oauthclient:mk_authorization_url(C), + io:format("~n~n~nVisit this URL:~n~s~n~n", [URL]), + {ok, C2} + end, + + fun(C) -> + {ok, [Code]} = io:fread('...and enter the verifier code: ', "~s"), + {C2, ok} = oauthclient:authorization_completed(C, Code), + {ok, C2} + end, + + fun(C) -> + {_C, {ok, _}} = oauthclient:get_access_token(C), + io:format("got access token~n"), + {ok, done} + end + ] + ). + +%% + +thread_state(Seed, SeedF, []) -> SeedF(Seed); +thread_state(Seed, SeedF, [F|Funs]) -> + {ok, NewSeed} = F(Seed), + case NewSeed of + done -> ok; + NS -> thread_state(SeedF(NS), SeedF, Funs) + end.