Skip to content

Commit

Permalink
Merge tag '0.1.0' into develop
Browse files Browse the repository at this point in the history
Publish to Hex
  • Loading branch information
yurrriq committed Mar 5, 2018
2 parents ce63bfd + 9508c61 commit 5aaea42
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 104 deletions.
75 changes: 35 additions & 40 deletions README.md
Expand Up @@ -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
```
104 changes: 96 additions & 8 deletions doc/elli_basicauth.md
Expand Up @@ -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.

<a name="description"></a>
<a name="types"></a>

## Data Types ##




### <a name="type-auth_fun">auth_fun()</a> ###


__abstract datatype__: `auth_fun()`

A user-configurable authentication function.



### <a name="type-auth_status">auth_status()</a> ###


__abstract datatype__: `auth_status()`

The result of an <code><a href="#type-auth_fun">auth_fun()</a></code>.



### <a name="type-config">config()</a> ###


__abstract datatype__: `config()`

A property list of options.
The configurable options are:



<dt><code>auth_fun</code></dt>

## Description ##
This middleware provides basic authentication to protect
Reqs based on a user-configured authentication function<a name="index"></a>



<dd>An <code><a href="#type-auth_fun">auth_fun()</a></code></dd>




<dt><code>auth_realm</code></dt>




<dd>A binary <a href="https://tools.ietf.org.md/rfc1945#section-11">realm</a>.</dd>





### <a name="type-credentials">credentials()</a> ###


<pre><code>
credentials() = {undefined, undefined} | {Username::binary(), Password::binary()}
</code></pre>

<a name="index"></a>

## Function Index ##


<table width="100%" border="1" cellspacing="0" cellpadding="2" summary="function index"><tr><td valign="top"><a href="#handle-2">handle/2</a></td><td></td></tr><tr><td valign="top"><a href="#handle_event-3">handle_event/3</a></td><td></td></tr></table>
<table width="100%" border="1" cellspacing="0" cellpadding="2" summary="function index"><tr><td valign="top"><a href="#default_auth_fun-2">default_auth_fun/2</a></td><td>Default to <code>forbidden</code>, in case of missing <code>auth_fun</code> config.</td></tr><tr><td valign="top"><a href="#handle-2">handle/2</a></td><td>Protect <code>Req</code> based on the configured <code>auth_fun</code>.</td></tr><tr><td valign="top"><a href="#handle_event-3">handle_event/3</a></td><td>No-op to satisfy the <code>elli_handler</code> behaviour.</td></tr></table>


<a name="functions"></a>

## Function Details ##

<a name="default_auth_fun-2"></a>

### default_auth_fun/2 ###

<pre><code>
default_auth_fun(Req, Credentials) -&gt; AuthStatus
</code></pre>

<ul class="definitions"><li><code>Req = <a href="http://raw.github.com/elli-lib/elli/develop/doc/elli.md#type-req">elli:req()</a></code></li><li><code>Credentials = <a href="#type-credentials">credentials()</a></code></li><li><code>AuthStatus = <a href="#type-auth_status">auth_status()</a></code></li></ul>

Default to `forbidden`, in case of missing `auth_fun` config.

<a name="handle-2"></a>

### handle/2 ###

`handle(Req, Config) -> any()`
<pre><code>
handle(Req::<a href="http://raw.github.com/elli-lib/elli/develop/doc/elli.md#type-req">elli:req()</a>, Config::<a href="#type-config">config()</a>) -&gt; <a href="http://raw.github.com/elli-lib/elli/develop/doc/elli_handler.md#type-result">elli_handler:result()</a>
</code></pre>
<br />

Protect `Req` based on the configured `auth_fun`.
If none is given, the default authentication is `forbidden`.

<a name="handle_event-3"></a>

### handle_event/3 ###

`handle_event(X1, X2, X3) -> any()`
<pre><code>
handle_event(Event::<a href="http://raw.github.com/elli-lib/elli/develop/doc/elli_handler.md#type-event">elli_handler:event()</a>, Args::list(), Config::<a href="#type-config">config()</a>) -&gt; ok
</code></pre>
<br />

No-op to satisfy the `elli_handler` behaviour. Return `ok`.

137 changes: 91 additions & 46 deletions 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:
%% <dl>
%% <dt>`auth_fun'</dt>
%% <dd>An {@type auth_fun()}</dd>
%% <dt>`auth_realm'</dt>
%% <dd>A binary <a href="https://tools.ietf.org/html/rfc1945#section-11">realm</a>.</dd>
%% </dl>
-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.

0 comments on commit 5aaea42

Please sign in to comment.