ejabberd-contrib/mod_openid/src/mod_openid.erl

252 lines
8.3 KiB
Erlang

%%%----------------------------------------------------------------------
%%% File : mod_openid.erl
%%% Author : Olivier Goffart <ogoffart@kde.org>
%%% Purpose : Open id provider using XEP-0070
%%% Created : 24 Dec 2007 Olivier Goffart
%%%----------------------------------------------------------------------
%%% Copyright (c) 2007-2008 Olivier Goffart
%%%----------------------------------------------------------------------
%% WARNING: secret/1 and new_assoc/2 MUST be implemented correctly for security issue
-module(mod_openid).
-author('ogoffart@kde.org').
%% External exports
-export([process/2]).
-include("ejabberd.hrl").
-include("jlib.hrl").
-include("web/ejabberd_http.hrl").
-include("web/ejabberd_web_admin.hrl").
-record(profile, {identity, server, lang, jid}).
-define(MYDEBUG(Format,Args),io:format("D(~p:~p:~p) : "++Format++"~n",
[calendar:local_time(),?MODULE,?LINE]++Args)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% PART 1 : OpenID
process([Jid], #request{ q = Query, lang = Lang} = Request) ->
%%?MYDEBUG("Auth Failed ~p ~n", [C]),
%%[{server, Server}] = ets:lookup(mod_openid, server)
JJid = jlib:string_to_jid(Jid),
Server = "http://" ++ JJid#jid.server ++ ":5280/openid",
Profile = #profile{identity = Server ++"/"++ Jid,
server = Server ++ "/" ++ Jid,
lang = Lang, jid= JJid},
case lists:keysearch("openid.mode", 1, Query) of
{value, {_, "associate"}} -> associate(Query,Profile);
{value, {_, "checkid_immediate"}} -> checkid_immediate(Query,Profile);
{value, {_, "checkid_setup"}} -> checkid_setup(Request,Profile);
{value, {_, "check_authentication"}} -> check_authentication(Query,Profile);
_ -> default_page(Profile)
end;
process(_, _) ->error_400("Invalid identity").
default_page(Profile) ->
make_xhtml([{xmlelement,"h1",[],[{xmlcdata, "400 Bad Request"}]}],Profile).
associate(_,Profile) ->
not_implemented(Profile).
checkid_immediate(_,Profile) ->
not_implemented(Profile).
checkid_setup(#request{ q = Query} = Request, Profile) ->
case lists:keysearch("openid.return_to", 1, Query) of
{value, {_, ReturnTo}} ->
case catch verify_id(Request,Profile) of
verified -> checkid2(Request,Profile,ReturnTo);
{return, Value} -> Value;
_ -> redirect_reply([{"openid_mode","error"},
{"openid_error","InternalError"}], ReturnTo)
end;
_ -> error_400("Missing 'openid.return_to'")
end.
%% Helper for checkid_setup
checkid2(#request{ q = Query} = _Request, Profile,ReturnTo) ->
case lists:keysearch("openid.identity", 1, Query) of
{value, {_, Ident}} when Ident == Profile#profile.identity ->
{AssocHandle,Secret} =
case lists:keysearch("openid.assoc_handle", 1, Query) of
{value, {_, V}} -> case secret(V) of
{ok, S} -> {V,S};
false -> new_assoc()
end;
false -> new_assoc()
end,
_TrustRoot = case lists:keysearch("openid.trust_root", 1, Query) of
{value, {_, Vs}} -> Vs;
false -> ReturnTo
end,
Params = [{"identity",Ident} ,
{"return_to",ReturnTo}, {"assoc_handle", AssocHandle} ],
{Signed,Sig} = make_signature(Params,Secret),
redirect_reply(
lists:map(fun({K,V}) -> {"openid_" ++ K, V} end, Params)
++ [{"openid_mode","id_res"},
{"openid_signed", Signed},
{"openid_sig", Sig}],
ReturnTo);
_ -> redirect_reply([{"openid_mode","error"},
{"openid_error","WrongIdentity"}],
ReturnTo)
end.
check_authentication(Query, _Profile) ->
case lists:keysearch("openid.assoc_handle", 1, Query) of
{value, {_, AssocHandle} } ->
case lists:keysearch("openid.sig", 1, Query) of
{value, {_, Sig} } ->
case lists:keysearch("openid.signed", 1, Query) of
{value, {_, Signed} } ->
direct_reply([{"openid.mode","id_res"},
{"is_valid",
check_authentication2(AssocHandle, Sig,
Signed, Query)}]);
false -> error_reply("missing sig")
end;
false -> error_reply("missing sig")
end;
false -> error_reply("missing handle")
end.
%% Helper for check_authentication
%% return "true" if the authentication is valid, "false" otherwhise
check_authentication2(AssocHandle, Sig, Signed, Query) ->
case secret(AssocHandle) of
{ok, Secret} ->
case catch make_signature(retrieve_params(Signed,Query),Secret) of
{Signed,Sig} -> "true";
_ -> "false"
end;
_ -> "false"
end.
%% Fields is a list of fields which should be in the query as openid.Field
%% return the list of argument [{Key,Value}] as they appears in the query
retrieve_params(Fields,Query) ->
FList = re:split(Fields, ",", [{return, list}]),
retrieve_params_recurse(FList,Query).
retrieve_params_recurse([],_) -> [];
retrieve_params_recurse([Key | Tail ], Query) ->
{value, {_, Value} } = lists:keysearch("openid." ++ Key, 1, Query),
[ {Key, Value} | retrieve_params_recurse(Tail,Query) ].
not_implemented(Profile) ->
make_xhtml([{xmlelement,"h1",[],[{xmlcdata, "NOT IMPLEMENTED"}]}],Profile).
make_xhtml(Els, #profile{lang=Lang} = Profile) ->
{200, [html],
{xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"},
{"xml:lang", Profile#profile.lang},
{"lang", Profile#profile.lang}],
[{xmlelement, "head", [],
[?XCT("title", "ejabberd OpenId Provider"),
{xmlelement, "meta", [{"http-equiv", "Content-Type"},
{"content", "text/html; charset=utf-8"}], []},
{xmlelement, "link", [{"rel", "openid.server"},
{"href", Profile#profile.server}], []},
{xmlelement, "link", [{"rel", "openid.delegate"},
{"href", Profile#profile.identity}], []}]},
?XE("body", Els) ]}}.
error_400(Message) ->
{400, [], ejabberd_web:make_xhtml([{xmlelement,"h1",[],
[{xmlcdata, "400 Bad Request"}]},
{xmlelement,"p",[],[{xmlcdata, Message}]}])}.
%% Ask the user agent to go to the ReturnTo URL with the specified
%% Params in the query
redirect_reply(Params, ReturnTo) ->
Delim = case lists:member($?, ReturnTo) of
true -> $&;
false -> $?
end,
{303, [{"Location", ReturnTo ++ build_query(Params, Delim)}], []}.
%% Given a list of {Key,Value}, construct the query string, starting
%% with Delim (either '?' or '&')
build_query([ {Key,Value} | Tail ], Delim) ->
[Delim] ++ Key ++ "=" ++ Value ++ build_query(Tail, $&);
build_query([], _) ->
[].
%% return the parameters directly in the HTTP reply
direct_reply(Params) ->
{200, [], build_reply(Params)}.
%% Given a list of {Key,Value}, return the string which should
%% appears in the dirrect reply.
build_reply([ {Key,Value} | Tail ]) ->
Key ++ ":" ++ Value ++ "\n" ++ build_reply(Tail);
build_reply([]) -> [].
%% Direct reply of an error
error_reply(Message) ->
{400, [], "error:" ++ Message ++ "\n"}.
%% Given a list of parameters, sign them.
make_signature(Param, Secret) ->
{field_list(Param),
jlib:encode_base64(binary_to_list(crypto:sha_mac(Secret, build_reply(Param))))}.
%% Given a list of parameters, return a string containing the list of
%% key separated by comas.
field_list([]) -> [];
field_list([ {Key,_Value} ]) -> Key;
field_list([ {Key,_Value} | Tail ]) ->
Key ++ "," ++ field_list(Tail).
%% TODO: thoses function need to be implemented for security
%% given an association key, return {ok, Secret} or false
secret(_) -> {ok, "Secret"}.
%% create a new association and return {Key, Secret}
new_assoc() -> {"assoc", "Secret"}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% PART 2 : Glue
%% return "verified" if the user agent is authorized, otherwhise, return an HTTP reply.
verify_id(#request{auth = Auth} = _Request, #profile{ jid = Jid} = _Profile) ->
%% TODO verify that the Jid in the identity is the same as the Jid in the profile
Exp = {Jid#jid.luser, Jid#jid.lserver},
case get_auth(Auth) of
Exp ->
verified;
C ->
?MYDEBUG("Auth Failed ~p ~n", [C]),
{return, {401,
[{"WWW-Authenticate", "basic realm=\"ejabberd-mod_openid\""}],
ejabberd_web:make_xhtml([{xmlelement, "h1", [],
[{xmlcdata, "401 Unauthorized"}]}])}}
end.
%% return the {User,Server} jid if the authentification is correct, or unauthorized
get_auth(Auth) ->
case Auth of
{SJID, P} ->
case jlib:string_to_jid(SJID) of
error ->
unauthorized;
#jid{user = U, server = S} ->
case ejabberd_auth:check_password(U, S, P) of
true ->
{U, S};
false ->
unauthorized
end
end;
_ ->
unauthorized
end.