Skip to content

Commit

Permalink
Add support of hashed passwords in yaws configuration and in .yaws_au…
Browse files Browse the repository at this point in the history
…th files

Now, it is possible to used hashed password to define user's credential. In the
yaws configuration, you can add following lines is <auth> sections:

  user = "User:{Algo}Base64Hash"

and in .yaws_auth files:

  {"User", "Algo", "Base64Hash"}.

Algo is one of: md5 | ripemd160 | sha | sha224 | sha256 | sha384 | sha512

and Base64Hash is the result of the hash functions, encoded in base64. For
example:

  "/N4rLtula/QIYB+3If6bXDONEO5CnqBPrlURto+/j7k=" for "bar" password.

yaws startup script has been updated to add a way to generate user's credential:

  $> yaws --auth --algo sha256 USER
  baz

  User's credential successfully generated:
      Put this line in your Yaws config (in <auth> section): user = "USER:{sha256}uqWglk0zIPvAxqkiFARTyFE+okq4/QV3A0gEqWckgJY="

      Or in a .yaws_auth file: {"USER", "sha256", "uqWglk0zIPvAxqkiFARTyFE+okq4/QV3A0gEqWckgJY="}.
  • Loading branch information
capflam committed Sep 20, 2016
1 parent 215ad88 commit 6d6b170
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 30 deletions.
17 changes: 13 additions & 4 deletions doc/yaws.tex
Expand Up @@ -3514,9 +3514,16 @@ \section{Server Part}
\verb+{appmod, ?MODULE}+ and the \verb+out401/1+ function in the
same module would return \verb+{redirect_local, "/login.html"}+.

\item \verb+user = User:Password+ --- Inside this directory, the
user \verb+User+ has access if the user supplies the password
\verb+Password+ in the popup dialogue presented by the browser.
\item \verb+user = User:Password | "User:{Algo}Hash"+ --- Inside
this directory, the user \verb+User+ has access if the user
supplies the password \verb+Password+ in the popup dialogue
presented by the browser. It is also possible to provide a
hashed password, encoded in base64. In that case, the algorithm
used to hash the password must be set. \verb+Algo+ must be a one
of the following algorithms:
\begin{verbatim}
md5 | ripemd160 | sha | sha224 | sha256 | sha384 | sha512
\end{verbatim}
We can obviously have several of these value inside a single
<auth> </auth> pair.

Expand Down Expand Up @@ -3609,6 +3616,7 @@ \section{Directives included from .yaws\_auth files}

\begin{verbatim}
{User, Password}.
{User, Algo, Hash}.
{realm, String}.
{pam, Atom}.
{authmod, Atom}.
Expand All @@ -3618,7 +3626,8 @@ \section{Directives included from .yaws\_auth files}
{order, allow_deny|deny_allow}.
\end{verbatim}

where \verb+User+ and \verb+Password+ are strings with double quotes.
where \verb+User+, \verb+Password+, \verb+Algo+ and \verb+Hash+ are
strings with double quotes.

The \verb+.yaws_auth+ file mechanism is recursive, so any
subdirectories of \verb+Dir+ are also automatically
Expand Down
18 changes: 14 additions & 4 deletions man/yaws.conf.5
Expand Up @@ -1355,11 +1355,21 @@ callback would check if a valid cookie header is present, if not it would return
.RE

.IP
\fBuser = User:Password\fR
\fBuser = User:Password | "User:{Algo}Hash"\fR
.RS 12
Inside this directory, the user User has access if the user supplies the
password Password in the popup dialogue presented by the browser. We can
obviously have several of these value inside a single <auth> </auth> pair.
Inside this directory, the user \fIUser\fR has access if the user supplies the
password \fIPassword\fR in the popup dialogue presented by the browser. It is
also possible to provide a hashed password, encoded in base64. In that case, the
algorithm used to hash the password must be set. \fIAlgo\fR must be a one of the
following algorithms:

.nf
md5 | ripemd160 | sha | sha224 | sha256 | sha384 | sha512
.fi

We can obviously have several of these value inside a single <auth> </auth>
pair.

.RE

.IP
Expand Down
29 changes: 29 additions & 0 deletions scripts/yaws.template
Expand Up @@ -80,6 +80,7 @@ help()
echo " yaws --wait-started[=secs] [--id ID] -- wait for daemon to be ready"
echo " yaws --wait-stopped[=secs] [--id ID] -- wait for daemon to be stopped"
echo " yaws --stats [--id ID] -- show daemon statistics"
echo " yaws --auth USER [--algo ALGO] -- generate credential for a user"
exit 1
}

Expand All @@ -106,6 +107,9 @@ checkargs=""
configtest=""
configtestcmd=""
verbose=""
auth=""
auth_user=""
auth_algo="sha256"
program=$0

wait_started() {
Expand Down Expand Up @@ -374,6 +378,21 @@ while [ $# -gt 0 ]
--umask)
umask $1
shift;;
-auth|--auth)
if [ -z "$1" ]; then
echo error: missing user argument
help
fi
auth="$erl -noshell -pa ${yawsdir}${delim}ebin -s yaws_ctl auth"
auth_user=$1
shift;;
-algo|--algo)
if [ -z "$1" ]; then
echo error: missing algo argument
help
fi
auth_algo="$1"
shift;;
*)
if [ -z "$check" ]; then
help
Expand Down Expand Up @@ -420,6 +439,16 @@ if [ -n "$configtest" ]; then
fi
fi

if [ -n "$auth" ]; then
echo "Enter user password:"
read passwd
if [ -z $passwd ]; then
echo error: empty password
exit 1
fi
exec $auth $auth_user $auth_algo $passwd
fi

if [ -n "$ex" ]; then
[ -n "$id" ] && execid=$id || execid=default
exec $ex $execid
Expand Down
65 changes: 58 additions & 7 deletions src/yaws_config.erl
Expand Up @@ -274,6 +274,21 @@ parse_yaws_auth_file([{User, Password}|T], Auth0)
end,
parse_yaws_auth_file(T, Auth0#auth{users = Users});

parse_yaws_auth_file([{User, Algo, B64Hash}|T], Auth0)
when is_list(User), is_list(Algo), is_list(B64Hash) ->
case parse_auth_user(User, Algo, B64Hash) of
{ok, Res} ->
Users = case lists:member(Res, Auth0#auth.users) of
true -> Auth0#auth.users;
false -> [Res | Auth0#auth.users]
end,
parse_yaws_auth_file(T, Auth0#auth{users = Users});
{error, Reason} ->
error_logger:format("Failed to parse user line ~p: ~p~n",
[{User, Algo, B64Hash}, Reason]),
parse_yaws_auth_file(T, Auth0)
end;

parse_yaws_auth_file([{allow, all}|T], Auth0) ->
Auth1 = case Auth0#auth.acl of
none -> Auth0#auth{acl={all, [], deny_allow}};
Expand Down Expand Up @@ -2099,16 +2114,15 @@ fload(FD, server_auth, GC, C, Lno, Chars) ->
fload(FD, server_auth, GC, C1, Lno+1, ?NEXTLINE);

["user", '=', User] ->
case (catch string:tokens(User, ":")) of
[Name, Passwd] ->
Hash = crypto:hash(sha256, Passwd),
case parse_auth_user(User, Lno) of
{Name, Algo, Hash} ->
Auth1 = Auth#auth{
users = [{Name, sha256, Hash}|Auth#auth.users]
users = [{Name, Algo, Hash}|Auth#auth.users]
},
C1 = C#sconf{authdirs=[Auth1|AuthDirs]},
fload(FD, server_auth, GC, C1, Lno+1, ?NEXTLINE);
_ ->
{error, ?F("Invalid user at line ~w", [Lno])}
{error, Str} ->
{error, Str}
end;

["allow", '=', "all"] ->
Expand Down Expand Up @@ -2971,7 +2985,6 @@ parse_redirect(_Path, _, _Mode, Lno) ->
{error, ?F("Bad redirect rule at line ~w", [Lno])}.



ssl_start() ->
case catch ssl:start() of
ok ->
Expand Down Expand Up @@ -3312,6 +3325,44 @@ parse_auth_ips([Str|Rest], Result) ->
_:_ -> parse_auth_ips(Rest, Result)
end.

parse_auth_user(User, Lno) ->
try
[Name, Passwd] = string:tokens(User, ":"),
case re:run(Passwd, "{([^}]+)}(.+)", [{capture,all_but_first,list}]) of
{match, [Algo, B64Hash]} ->
case parse_auth_user(Name, Algo, B64Hash) of
{ok, Res} ->
Res;
{error, bad_algo} ->
{error, ?F("Unsupported hash algorithm '~p' at line ~w",
[Algo, Lno])};
{error, bad_user} ->
{error, ?F("Invalid user at line ~w", [Lno])}
end;
_ ->
{Name, sha256, crypto:hash(sha256, Passwd)}
end
catch
_:_ ->
{error, ?F("Invalid user at line ~w", [Lno])}
end.

parse_auth_user(User, Algo, B64Hash) ->
try
if
Algo == "md5" orelse Algo == "sha" orelse
Algo == "sha224" orelse Algo == "sha256" orelse
Algo == "sha384" orelse Algo == "sha512" orelse
Algo == "ripemd160" ->
Hash = base64:decode(B64Hash),
{ok, {User, list_to_atom(Algo), Hash}};
true ->
{error, unsupported_algo}
end
catch
_:_ -> {error, bad_user}
end.


subconfigfiles(FD, Name, Lno) ->
{ok, Config} = file:pid2name(FD),
Expand Down
25 changes: 23 additions & 2 deletions src/yaws_ctl.erl
Expand Up @@ -19,7 +19,7 @@
-export([start/2, actl_trace/1]).
-export([ls/1,hup/1,stop/1,status/1,load/1,
check/1,trace/1, debug_dump/1, stats/1, running_config/1,
configtest/1]).
configtest/1, auth/1]).
%% internal
-export([run/1, aloop/3, handle_a/3]).

Expand Down Expand Up @@ -557,7 +557,6 @@ running_config([SID]) ->
actl(SID, running_config).

configtest([File]) ->

Env = #env{debug = false, conf = {file, File}},
case catch yaws_config:load(Env) of
{ok, _GC, _SCs} ->
Expand All @@ -570,3 +569,25 @@ configtest([File]) ->
io:format("Syntax error in file ~p:~n~p~n", [File, Other]),
timer:sleep(100),erlang:halt(1)
end.

auth([User, Algo, Passwd]) ->
if
Algo == md5 orelse Algo == sha orelse
Algo == sha224 orelse Algo == sha256 orelse
Algo == sha384 orelse Algo == sha512 orelse
Algo == ripemd160 ->
Hash = crypto:hash(Algo, atom_to_list(Passwd)),
B64Hash = base64:encode(Hash),
io:format("~nUser's credential successfully generated:~n", []),
io:format("\tPut this line in your Yaws config (in <auth> section):"
" user = \"~s:{~s}~s\"~n~n",
[atom_to_list(User), atom_to_list(Algo), B64Hash]),
io:format("\tOr in a .yaws_auth file:"
" {\"~s\", \"~s\", \"~s\"}.~n",
[atom_to_list(User), atom_to_list(Algo), B64Hash]),
timer:sleep(100),erlang:halt(0);
true ->
io:format("Unsupported Hash algorithm ~p~n"
"\tUse: md5 | ripemd160 | sha | sha224 | sha256 | sha384 | sha512 ~n", [Algo]),
timer:sleep(100),erlang:halt(1)
end.
15 changes: 5 additions & 10 deletions src/yaws_server.erl
Expand Up @@ -2172,16 +2172,11 @@ handle_auth(ARG, {User, Password, OrigString},

handle_auth(ARG, {User, Password, OrigString},
Auth_methods = #auth{users = Users}, Ret) when Users /= [] ->
case lists:keyfind(User, 1, Users) of
{User, Algo, Hash} ->
case crypto:hash(Algo, Password) of
Hash ->
maybe_auth_log({ok, User}, ARG),
true;
_ ->
handle_auth(ARG, {User, Password, OrigString},
Auth_methods#auth{users = []}, Ret)
end;
F = fun({U, A, H}) -> (U == User andalso H == crypto:hash(A, Password)) end,
case lists:any(F, Users) of
true ->
maybe_auth_log({ok, User}, ARG),
true;
false ->
handle_auth(ARG, {User, Password, OrigString},
Auth_methods#auth{users = []}, Ret)
Expand Down
17 changes: 14 additions & 3 deletions testsuite/auth_SUITE.erl
Expand Up @@ -58,14 +58,17 @@ basic_auth(Config) ->
Url = testsuite:make_url(http, "127.0.0.1", Port, "/test1/a.txt"),
Auth1 = auth_header("foo", "baz"),
Auth2 = auth_header("foo", "bar"),
Auth3 = auth_header("foo", "sha256baz"),
Auth4 = auth_header("foo", "md5baz"),

{ok, {StatusLine, Hdrs, _}} = testsuite:http_get(Url),
?assertMatch({_, 401, _}, StatusLine),
?assertEqual("Basic realm=\"test1\"", proplists:get_value("www-authenticate", Hdrs)),

?assertMatch({ok, {{_,401,_}, _, _}}, testsuite:http_get(Url, [Auth1])),

?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url, [Auth2])),
?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url, [Auth3])),
?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url, [Auth4])),
ok.

basic_auth_with_docroot(Config) ->
Expand Down Expand Up @@ -169,20 +172,28 @@ yaws_auth_hidden_file(Config) ->
Url1 = testsuite:make_url(http, "127.0.0.1", Port, "/test10/a.txt"),
Url2 = testsuite:make_url(http, "127.0.0.1", Port, "/test10/b.txt"),
Url3 = testsuite:make_url(http, "127.0.0.1", Port, "/test10/c.txt"),
Auth1 = auth_header("foo", "bar"),
Auth1 = auth_header("foo", "baz"),
Auth2 = auth_header("foo", "bar"),
Auth3 = auth_header("foo", "sha256baz"),
Auth4 = auth_header("foo", "md5baz"),

{ok, {StatusLine1, Hdrs1, _}} = testsuite:http_get(Url1),
?assertMatch({_, 401, _}, StatusLine1),
?assertEqual("Basic realm=\"test10\"", proplists:get_value("www-authenticate", Hdrs1)),

?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url1, [Auth1])),
?assertMatch({ok, {{_,401,_}, _, _}}, testsuite:http_get(Url1, [Auth1])),
?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url1, [Auth2])),
?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url1, [Auth3])),
?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url1, [Auth4])),

{ok, {StatusLine2, Hdrs2, _}} = testsuite:http_get(Url2),
?assertMatch({_, 401, _}, StatusLine2),
?assertEqual("Basic realm=\"test10\"", proplists:get_value("www-authenticate", Hdrs2)),

?assertMatch({ok, {{_,401,_}, _, _}}, testsuite:http_get(Url2, [Auth1])),
?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url2, [Auth2])),
?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url2, [Auth3])),
?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url2, [Auth4])),

?assertMatch({ok, {{_,200,_}, _, _}}, testsuite:http_get(Url3)),
ok.
Expand Down
2 changes: 2 additions & 0 deletions testsuite/auth_SUITE_data/templates/yaws.conf
Expand Up @@ -22,6 +22,8 @@ keepalive_timeout = 10000
realm = test1
dir = /test1
user = foo:bar
user = "foo:{sha256}szTXjROUAlPTDWcL0pxczKpZCGFPK0W5qa0EAkp8l5Q=" #sha256baz
user = "foo:{md5}C5GxiXRrcq9Eih5W1CqNaA==" #md5baz
</auth>

<auth>
Expand Down
2 changes: 2 additions & 0 deletions testsuite/auth_SUITE_data/www1/test10/.yaws_auth
Expand Up @@ -3,3 +3,5 @@
{file, "a.txt"}.
{file, "b.txt"}.
{"foo", "bar"}.
{"foo", "sha256", "szTXjROUAlPTDWcL0pxczKpZCGFPK0W5qa0EAkp8l5Q="}. %% sha256baz
{"foo", "md5", "C5GxiXRrcq9Eih5W1CqNaA=="}. %% md5baz

0 comments on commit 6d6b170

Please sign in to comment.