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 ## -
handle/2
handle_event/3
+
default_auth_fun/2Default to forbidden, in case of missing auth_fun config.
handle/2Protect Req based on the configured auth_fun.
handle_event/3No-op to satisfy the elli_handler behaviour.
## 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) ->