diff --git a/README.md b/README.md
index b6d937b..f8d4812 100644
--- a/README.md
+++ b/README.md
@@ -29,45 +29,40 @@
```
-Use it together with the [Elli webserver](https://github.com/knutin/elli)
-like this:
-
## Example
-```erlang
--module(my_elli_stuff).
-
--export([start_link/0, auth_fun/3]).
-
-
-start_link() ->
- BasicauthConfig = [
- {auth_fun, fun my_elli_stuff:auth_fun/3},
- {auth_realm, <<"Admin Area">>} % optional
- ],
-
- Config = [
- {mods, [
- {elli_basicauth, BasicauthConfig},
- {elli_example_callback, []}
- ]}
- ],
-
- elli:start_link([{callback, elli_middleware},
- {callback_args, Config}]).
-
-
-auth_fun(Req, User, Password) ->
- case elli_request:path(Req) of
- [<<"protected">>] -> password_check(User, Password);
- _ -> ok
- end.
-
-
-password_check(User, Password) ->
- case {User, Password} of
- {undefined, undefined} -> unauthorized;
- {<<"admin">>, <<"secret">>} -> ok;
- {User, Password} -> forbidden
- end.
-```
+- Start an Erlang shell with elli and elli_basicauth loaded.
+
+ ```fish
+ rebar3 as test shell
+ ```
+
+- Start [elli_basicauth_example](./test/elli_basicauth_example.erl).
+
+ ```erlang
+ 1> {ok, Pid} = elli_basicauth_example:start_link().
+ ```
+
+- Make requests, e.g. using [HTTPie](https://httpie.org/).
+ ```fish
+ http :8080/protected
+ ```
+ ```http
+ HTTP/1.1 401 Unauthorized
+ Connection: Keep-Alive
+ Content-Length: 12
+ WWW-Authenticate: Basic realm="Admin Area"
+
+ Unauthorized
+ ```
+
+ ```fish
+ http -a user:pass :8080/protected
+ ```
+ ```http
+ HTTP/1.1 403 Forbidden
+ Connection: Keep-Alive
+ Content-Length: 9
+
+ Forbidden
+ ```
diff --git a/doc/elli_basicauth.md b/doc/elli_basicauth.md
index e1467b1..352c31b 100644
--- a/doc/elli_basicauth.md
+++ b/doc/elli_basicauth.md
@@ -2,40 +2,128 @@
# Module elli_basicauth #
* [Description](#description)
+* [Data Types](#types)
* [Function Index](#index)
* [Function Details](#functions)
Elli basicauth middleware.
+Copyright (c) 2013, Martin Rehfeld; 2018, elli-lib team
+
+This middleware provides basic authentication to protect
+requests, based on a user-configured authentication function.
+
__Behaviours:__ [`elli_handler`](https://github.com/elli-lib/elli/blob/develop/doc/elli_handler.md).
-__Authors:__ Martin Rehfeld.
+__Authors:__ Martin Rehfeld, Eric Bailey.
-
+
+
+## Data Types ##
+
+
+
+
+### auth_fun() ###
+
+
+__abstract datatype__: `auth_fun()`
+
+A user-configurable authentication function.
+
+
+
+### auth_status() ###
+
+
+__abstract datatype__: `auth_status()`
+
+The result of an auth_fun()
.
+
+
+
+### config() ###
+
+
+__abstract datatype__: `config()`
+
+A property list of options.
+The configurable options are:
+
+
+
+
auth_fun
-## Description ##
-This middleware provides basic authentication to protect
-Reqs based on a user-configured authentication function
+
+
+
+An auth_fun()
+
+
+
+
+auth_realm
+
+
+
+
+A binary realm.
+
+
+
+
+
+### credentials() ###
+
+
+
+credentials() = {undefined, undefined} | {Username::binary(), Password::binary()}
+
+
+
## Function Index ##
-
+
## Function Details ##
+
+
+### default_auth_fun/2 ###
+
+
+default_auth_fun(Req, Credentials) -> AuthStatus
+
+
+
+
+Default to `forbidden`, in case of missing `auth_fun` config.
+
### handle/2 ###
-`handle(Req, Config) -> any()`
+
+handle(Req::elli:req(), Config::config()) -> elli_handler:result()
+
+
+
+Protect `Req` based on the configured `auth_fun`.
+If none is given, the default authentication is `forbidden`.
### handle_event/3 ###
-`handle_event(X1, X2, X3) -> any()`
+
+handle_event(Event::elli_handler:event(), Args::list(), Config::config()) -> ok
+
+
+
+No-op to satisfy the `elli_handler` behaviour. Return `ok`.
diff --git a/src/elli_basicauth.erl b/src/elli_basicauth.erl
index ab19044..b23f711 100644
--- a/src/elli_basicauth.erl
+++ b/src/elli_basicauth.erl
@@ -1,87 +1,132 @@
%% @doc Elli basicauth middleware
+%% @author Martin Rehfeld
+%% @author Eric Bailey
+%% @copyright 2013, Martin Rehfeld; 2018, elli-lib team
%%
%% This middleware provides basic authentication to protect
-%% Reqs based on a user-configured authentication function
-%% @author Martin Rehfeld
-
+%% requests, based on a user-configured authentication function.
-module(elli_basicauth).
-behaviour(elli_handler).
--export([handle/2, handle_event/3]).
+-export([handle/2, handle_event/3, default_auth_fun/2]).
+-export_type([auth_fun/0, auth_status/0, config/0, credentials/0]).
-handle(Req, Config) ->
- {User, Password} = credentials(Req),
- case apply(auth_fun(Config), [Req, User, Password]) of
- unauthorized ->
- throw({401,
- [{<<"WWW-Authenticate">>, auth_realm(Config)}],
- <<"Unauthorized">>});
+%% @type auth_fun(). A user-configurable authentication function.
+-type auth_fun() :: fun((Req :: elli:req(),
+ Credentials :: credentials()) ->
+ AuthStatus :: auth_status()).
+
+
+-type credentials() :: {undefined, undefined} |
+ {Username :: binary(),
+ Password :: binary()}.
+
+
+%% @type auth_status(). The result of an {@type auth_fun()}.
+-type auth_status() :: ok |
+ unauthorized |
+ forbidden |
+ hidden.
+
- forbidden ->
- throw({403, [], <<"Forbidden">>});
+%% @type config(). A property list of options.
+%% The configurable options are:
+%%
+%% - `auth_fun'
+%% - An {@type auth_fun()}
+%% - `auth_realm'
+%% - A binary realm.
+%%
+-type config() :: [{auth_fun, auth_fun()} |
+ {auth_realm, binary()} |
+ term()].
- hidden ->
- throw({404, [], <<>>});
- _ ->
- ignore
- end.
+-define(DEFAULT_CREDENTIALS, {undefined, undefined}).
-handle_event(_, _, _) ->
+-define(DEFAULT_REALM, <<"Secure Area">>).
+
+
+%% @doc Protect `Req' based on the configured `auth_fun'.
+%% If none is given, the default authentication is `forbidden'.
+-spec handle(elli:req(), config()) -> elli_handler:result().
+handle(Req, Config) ->
+ Credentials = credentials(Req),
+ Authentication = apply(auth_fun(Config), [Req, Credentials]),
+ do_handle(Authentication, Config).
+
+
+-spec do_handle(auth_status(), config()) -> elli_handler:result().
+do_handle(ok, _Config) ->
+ ignore;
+do_handle(unauthorized, Config) ->
+ Headers = [{<<"WWW-Authenticate">>, auth_realm(Config)}],
+ {401, Headers, <<"Unauthorized">>};
+do_handle(forbidden, _Config) ->
+ {403, [], <<"Forbidden">>};
+do_handle(hidden, _Config) ->
+ {404, [], <<>>}.
+
+
+%% @doc No-op to satisfy the `elli_handler' behaviour. Return `ok'.
+-spec handle_event(elli_handler:event(), list(), config()) -> ok.
+handle_event(_Event, _Args, _Config) ->
ok.
+%% @doc Default to `forbidden', in case of missing `auth_fun' config.
+-spec default_auth_fun(Req, Credentials) -> AuthStatus when
+ Req :: elli:req(),
+ Credentials :: credentials(),
+ AuthStatus :: auth_status().
+default_auth_fun(_Req, {_User, _Password}) ->
+ forbidden.
+
+
%%
%% INTERNAL HELPERS
%%
+-spec auth_fun(config()) -> auth_fun().
auth_fun(Config) ->
- proplists:get_value(auth_fun, Config,
- %% default to forbidden in case of missing auth_fun config
- fun (_Req, _User, _Password) ->
- forbidden
- end).
+ proplists:get_value(auth_fun, Config, fun default_auth_fun/2).
+-spec auth_realm(config()) -> binary().
auth_realm(Config) ->
- Realm = proplists:get_value(auth_realm, Config, <<"Secure Area">>),
+ Realm = proplists:get_value(auth_realm, Config, ?DEFAULT_REALM),
iolist_to_binary([<<"Basic realm=\"">>, Realm, <<"\"">>]).
+-spec credentials(elli:req()) -> credentials().
credentials(Req) ->
- case authorization_header(Req) of
- undefined ->
- {undefined, undefined};
-
- AuthorizationHeader ->
- credentials_from_header(AuthorizationHeader)
- end.
+ credentials_from_header(authorization_header(Req)).
+-spec authorization_header(elli:req()) -> undefined | binary().
authorization_header(Req) ->
elli_request:get_header(<<"Authorization">>, Req).
-credentials_from_header(AuthorizationHeader) ->
- case binary:split(AuthorizationHeader, <<$ >>) of
- [<<"Basic">>, EncodedCredentials] ->
- decoded_credentials(EncodedCredentials);
-
- _ ->
- {undefined, undefined}
- end.
+-spec credentials_from_header(undefined | binary()) -> credentials().
+credentials_from_header(<<"Basic ", EncodedCredentials/binary>>) ->
+ decoded_credentials(EncodedCredentials);
+credentials_from_header(_Authorization) ->
+ ?DEFAULT_CREDENTIALS.
+-spec decoded_credentials(binary()) -> credentials().
decoded_credentials(EncodedCredentials) ->
DecodedCredentials = base64:decode(EncodedCredentials),
- case binary:split(DecodedCredentials, <<$:>>) of
- [User, Password] ->
- {User, Password};
+ do_decoded_credentials(binary:split(DecodedCredentials, <<$:>>)).
+
- _ ->
- {undefined, undefined}
- end.
+-spec do_decoded_credentials([binary()]) -> credentials().
+do_decoded_credentials([User, Password]) ->
+ {User, Password};
+do_decoded_credentials(_Bins) ->
+ ?DEFAULT_CREDENTIALS.
diff --git a/test/elli_basicauth_example.erl b/test/elli_basicauth_example.erl
new file mode 100644
index 0000000..1ca1c6a
--- /dev/null
+++ b/test/elli_basicauth_example.erl
@@ -0,0 +1,39 @@
+-module(elli_basicauth_example).
+
+
+-export([start_link/0, auth_fun/2]).
+
+
+start_link() ->
+ Config = [
+ {auth_fun, fun ?MODULE:auth_fun/2},
+ {auth_realm, <<"Admin Area">>}
+ ],
+ CallbackArgs = [
+ {mods, [
+ {elli_basicauth, Config},
+ {elli_example_callback, []}
+ ]}
+ ],
+ elli:start_link([{callback, elli_middleware},
+ {callback_args, CallbackArgs}]).
+
+
+-spec auth_fun(Req, Credentials) -> AuthStatus when
+ Req :: elli:req(),
+ Credentials :: elli_basicauth:credentials(),
+ AuthStatus :: elli_basicauth:auth_status().
+auth_fun(Req, Credentials) ->
+ do_auth_fun(elli_request:path(Req), Credentials).
+
+
+do_auth_fun([<<"protected">>], Credentials) -> password_check(Credentials);
+do_auth_fun(_Path, _Credentials) -> ok.
+
+
+-spec password_check(Credentials) -> AuthStatus when
+ Credentials :: elli_basicauth:credentials(),
+ AuthStatus :: elli_basicauth:auth_status().
+password_check({undefined, undefined}) -> unauthorized;
+password_check({<<"admin">>, <<"secret">>}) -> ok;
+password_check(_Credentials) -> forbidden.
diff --git a/test/elli_basicauth_tests.erl b/test/elli_basicauth_tests.erl
index 8c33537..95db8c1 100644
--- a/test/elli_basicauth_tests.erl
+++ b/test/elli_basicauth_tests.erl
@@ -76,26 +76,25 @@ elli_handler_behaviour_test() ->
%%
basicauth_config() ->
- [{auth_fun, fun auth_fun/3}].
+ [{auth_fun, fun auth_fun/2}].
hidden_basicauth_config() ->
- [{auth_fun, fun hidden_auth_fun/3}].
+ [{auth_fun, fun hidden_auth_fun/2}].
basicauth_config_with_custom_realm() ->
- [{auth_fun, fun auth_fun/3},
- {auth_realm, <<"Members only">>}].
+ [{auth_realm, <<"Members only">>} | basicauth_config()].
+
-auth_fun(_Req, undefined, undefined) -> unauthorized;
-auth_fun(_Req, ?USER, ?PASSWORD) -> ok;
-auth_fun(_Req, _User, _Password) -> forbidden.
+auth_fun(_Req, {undefined, undefined}) -> unauthorized;
+auth_fun(_Req, {?USER, ?PASSWORD}) -> ok;
+auth_fun(_Req, _Credentials) -> forbidden.
-hidden_auth_fun(_Req, undefined, undefined) -> hidden;
-hidden_auth_fun(_Req, ?USER, ?PASSWORD) -> ok;
-hidden_auth_fun(_Req, _User, _Password) -> hidden.
+hidden_auth_fun(_Req, {?USER, ?PASSWORD}) -> ok;
+hidden_auth_fun(_Req, _Credentials) -> hidden.
no_auth(Expected) ->