252 lines
8.3 KiB
Erlang
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.
|
|
|