├── test ├── test_helper.exs └── plug_auth │ ├── access │ └── role_test.exs │ └── authentication │ ├── basic_test.exs │ └── token_test.exs ├── .gitignore ├── lib ├── plug_auth.ex └── plug_auth │ ├── access.ex │ ├── supervisor.ex │ ├── db_store.ex │ ├── authentication │ ├── ip_address.ex │ ├── utils.ex │ ├── basic.ex │ ├── database.ex │ └── token.ex │ ├── access │ └── role.ex │ └── credential_store.ex ├── mix.lock ├── LICENSE ├── mix.exs └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /docs 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /lib/plug_auth.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth do 2 | use Application 3 | 4 | @doc false 5 | def start(_type, _args) do 6 | PlugAuth.Supervisor.start_link() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"cowboy": {:hex, :cowboy, "1.0.0"}, 2 | "cowlib": {:hex, :cowlib, "1.0.1"}, 3 | "earmark": {:hex, :earmark, "0.1.12"}, 4 | "ex_doc": {:hex, :ex_doc, "0.6.2"}, 5 | "plug": {:hex, :plug, "0.10.0"}, 6 | "ranch": {:hex, :ranch, "1.0.0"}, 7 | "uuid": {:hex, :uuid, "1.0.0"}} 8 | -------------------------------------------------------------------------------- /lib/plug_auth/access.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Access do 2 | require Logger 3 | 4 | defprotocol RoleAdapter do 5 | @doc "Returns the role associated to `data` as atom" 6 | @fallback_to_any true 7 | def get_role(data) 8 | end 9 | 10 | defimpl RoleAdapter, for: Any do 11 | def get_role(map) when is_map(map), do: Map.get(map, :role) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/plug_auth/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Supervisor do 2 | @doc false 3 | def start_link() do 4 | Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) 5 | end 6 | 7 | @doc false 8 | def init(:ok) do 9 | import Supervisor.Spec 10 | 11 | children = [ 12 | worker(PlugAuth.CredentialStore, []) 13 | ] 14 | 15 | supervise(children, strategy: :one_for_one) 16 | end 17 | end -------------------------------------------------------------------------------- /lib/plug_auth/db_store.ex: -------------------------------------------------------------------------------- 1 | defprotocol PlugAuth.DbStore do 2 | @fallback_to_any true 3 | def get_user_data(resource, credentials, id_key) 4 | def put_credentials(resource, credentials, id_key) 5 | def delete_credentials(resource, credentials) 6 | end 7 | 8 | defimpl PlugAuth.DbStore, for: Any do 9 | require Logger 10 | def get_user_data(_, _, _), do: nil 11 | def put_credentials(_, _, _), do: nil 12 | def delete_credentials(_, _), do: nil 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Michele Balistreri 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /lib/plug_auth/authentication/ip_address.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Authentication.IpAddress do 2 | @moduledoc """ 3 | Implements ip address based authentication. To use add 4 | 5 | plug PlugAuth.Authentication.IpAddress, allow: ~w(127.0.0.1 192.168.1.200) 6 | 7 | to your pipeline. 8 | """ 9 | 10 | @behaviour Plug 11 | import Plug.Conn 12 | import PlugAuth.Authentication.Utils 13 | require Logger 14 | alias PlugAuth.Authentication.Utils 15 | 16 | def init(opts) do 17 | allow = Keyword.get(opts, :allow, []) 18 | error = Keyword.get(opts, :error, "Unauthorized IP Address") 19 | %{allow: allow, error: error} 20 | end 21 | 22 | def call(conn, %{allow: allow, error: error}) do 23 | ip = conn.peer |> elem(0) |> Utils.to_string 24 | if ip in allow do 25 | conn 26 | else 27 | Logger.warn "Unauthorized access from IP #{ip}" 28 | halt_with_error(conn, error) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/plug_auth/authentication/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Authentication.Utils do 2 | import Plug.Conn 3 | import Kernel, except: [to_string: 1] 4 | 5 | @param_key Application.get_env :plug_auth, :token_param_key, "param_key" 6 | 7 | def param_key, do: @param_key 8 | 9 | def assign_user_data(conn, user_data), do: assign(conn, :authenticated_user, user_data) 10 | def get_authenticated_user(conn), do: conn.assigns[:authenticated_user] 11 | def halt_with_error(conn, msg \\ "unauthorized") do 12 | conn 13 | |> send_resp(401, msg) 14 | |> halt 15 | end 16 | 17 | def get_first_req_header(conn, header), do: get_req_header(conn, header) |> header_hd 18 | 19 | def delete_token_session(conn) do 20 | case get_session(conn, param_key) do 21 | nil -> conn 22 | param -> put_session(conn, param, nil) 23 | end 24 | end 25 | 26 | defp header_hd([]), do: nil 27 | defp header_hd([head | _]), do: head 28 | 29 | def to_string({a,b,c,d}), do: "#{a}.#{b}.#{c}.#{d}" 30 | end 31 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :plug_auth, 7 | version: "0.0.4", 8 | elixir: "~> 1.0", 9 | deps: deps, 10 | package: package, 11 | description: description, 12 | docs: [readme: "README.md", main: "README"]] 13 | end 14 | 15 | def application do 16 | [ 17 | applications: [:logger, :cowboy, :plug], 18 | mod: {PlugAuth, []} 19 | ] 20 | end 21 | 22 | defp deps do 23 | [ 24 | {:cowboy, "~> 1.0.0"}, 25 | {:plug, "~> 1.1.0"}, 26 | {:uuid, "~> 1.0"}, 27 | {:earmark, "~> 0.1", only: :docs}, 28 | {:ex_doc, "~> 0.6", only: :docs}, 29 | ] 30 | end 31 | 32 | defp description do 33 | "A collection of authentication-related plugs" 34 | end 35 | 36 | defp package do 37 | [ 38 | contributors: ["Michele Balistreri", "Stephen Pallen"], 39 | licenses: ["ISC"], 40 | links: %{"GitHub" => "https://github.com/briksoftware/plug_auth"} 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/plug_auth/access/role_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Access.Role.Test do 2 | use ExUnit.Case, async: true 3 | use Plug.Test 4 | 5 | defmodule TestPlug do 6 | use Plug.Builder 7 | import Plug.Conn 8 | 9 | plug PlugAuth.Access.Role, roles: [:admin], error: "forbidden" 10 | plug :index 11 | 12 | defp index(conn, _opts), do: send_resp(conn, 200, "Authorized") 13 | end 14 | 15 | defp call(plug, role) do 16 | conn(:get, "/", []) 17 | |> assign(:authenticated_user, %{role: role}) 18 | |> plug.call([]) 19 | end 20 | 21 | defp assert_unauthorized(conn, content) do 22 | assert conn.status == 403 23 | assert conn.resp_body == content 24 | refute conn.assigns[:authenticated_role] 25 | end 26 | 27 | defp assert_authorized(conn, content) do 28 | assert conn.status == 200 29 | assert conn.resp_body == content 30 | assert conn.assigns[:authenticated_role] == :admin 31 | end 32 | 33 | test "request with no role" do 34 | conn = call(TestPlug, nil) 35 | assert_unauthorized conn, "forbidden" 36 | end 37 | 38 | test "request with invalid role" do 39 | conn = call(TestPlug, :guest) 40 | assert_unauthorized conn, "forbidden" 41 | end 42 | 43 | test "request with valid credentials" do 44 | conn = call(TestPlug, :admin) 45 | assert_authorized conn, "Authorized" 46 | end 47 | end -------------------------------------------------------------------------------- /lib/plug_auth/access/role.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Access.Role do 2 | @moduledoc """ 3 | Implements role-based access control. Authentication must occur before access control. 4 | 5 | ## Example: 6 | plug PlugAuth.Authentication.Basic, realm: "Secret world" 7 | plug PlugAuth.Access.Role, roles: [:admin] 8 | """ 9 | 10 | @behaviour Plug 11 | import Plug.Conn 12 | 13 | require Logger 14 | 15 | def init(opts) do 16 | roles = Keyword.fetch!(opts, :roles) 17 | error = Keyword.get(opts, :error, "HTTP Forbidden") 18 | %{roles: roles, error: error} 19 | end 20 | 21 | def call(conn, opts) do 22 | conn 23 | |> get_user 24 | |> get_role 25 | |> assert_role(opts[:roles], opts[:error]) 26 | end 27 | 28 | defp get_user(conn) do 29 | user_data = PlugAuth.Authentication.Utils.get_authenticated_user(conn) 30 | {conn, user_data} 31 | end 32 | defp get_role({conn, nil}), do: {conn, nil} 33 | defp get_role({conn, user}), do: {conn, PlugAuth.Access.RoleAdapter.get_role(user)} 34 | 35 | defp assert_role({conn, role}, roles, error) when is_list(role) do 36 | case Enum.filter(role, &(&1 in roles)) do 37 | [] -> 38 | halt_forbidden(conn, error) 39 | found -> 40 | assign(conn, :authenticated_role, found) 41 | end 42 | end 43 | 44 | defp assert_role({conn, role}, roles, error) do 45 | if role in roles do 46 | assign(conn, :authenticated_role, role) 47 | else 48 | halt_forbidden(conn, error) 49 | end 50 | end 51 | 52 | defp halt_forbidden(conn, error) do 53 | conn 54 | |> send_resp(403, error) 55 | |> halt 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/plug_auth/credential_store.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.CredentialStore do 2 | @doc """ 3 | Starts a new credentials store. 4 | """ 5 | require Logger 6 | alias PlugAuth.DbStore 7 | 8 | def start_link do 9 | Agent.start_link(&HashDict.new/0, name: __MODULE__) 10 | end 11 | 12 | @doc """ 13 | Gets the user data for the given credentials 14 | """ 15 | def get_user_data(credentials, nil, _) do 16 | get_data credentials 17 | end 18 | def get_user_data(credentials, db_model, id_key) do 19 | case get_data credentials do 20 | nil -> 21 | case DbStore.get_user_data(db_model.__struct__, credentials, id_key) do 22 | nil -> nil 23 | user_data -> 24 | Agent.update(__MODULE__, &HashDict.put(&1, credentials, user_data)) 25 | user_data 26 | end 27 | other -> 28 | other 29 | end 30 | end 31 | 32 | defp get_data(credentials), do: Agent.get(__MODULE__, &HashDict.get(&1, credentials)) 33 | 34 | @doc """ 35 | Puts the `user_data` for the given `credentials`. 36 | """ 37 | def put_credentials(credentials, user_data, id_key) do 38 | Agent.update(__MODULE__, &HashDict.put(&1, credentials, user_data)) 39 | DbStore.put_credentials(user_data, credentials, id_key) 40 | end 41 | 42 | @doc """ 43 | Deletes `credentials` from the store. 44 | 45 | Returns the current value of `credentials`, if `credentials` exists. 46 | """ 47 | def delete_credentials(credentials) do 48 | case get_data credentials do 49 | nil -> nil 50 | user_data -> 51 | DbStore.delete_credentials user_data, credentials 52 | Agent.get_and_update(__MODULE__, &HashDict.pop(&1, credentials)) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/plug_auth/authentication/basic_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Authentication.Basic.Test do 2 | use ExUnit.Case, async: true 3 | use Plug.Test 4 | 5 | defmodule TestPlug do 6 | use Plug.Builder 7 | import Plug.Conn 8 | 9 | plug PlugAuth.Authentication.Basic, realm: "Secret" 10 | plug :index 11 | 12 | defp index(conn, _opts), do: send_resp(conn, 200, "Authorized") 13 | end 14 | 15 | defp call(plug, headers) do 16 | conn(:get, "/", [], headers: headers) 17 | |> plug.call([]) 18 | end 19 | 20 | defp assert_unauthorized(conn, realm) do 21 | assert conn.status == 401 22 | assert get_resp_header(conn, "Www-Authenticate") == [~s{Basic realm="#{realm}"}] 23 | refute conn.assigns[:authenticated_user] 24 | end 25 | 26 | defp assert_authorized(conn, content) do 27 | assert conn.status == 200 28 | assert conn.resp_body == content 29 | assert conn.assigns[:authenticated_user] == %{role: :admin} 30 | end 31 | 32 | defp auth_header(creds) do 33 | {"authorization", "Basic #{Base.encode64(creds)}"} 34 | end 35 | 36 | setup do 37 | PlugAuth.Authentication.Basic.add_credentials("Admin", "SecretPass", %{role: :admin}) 38 | end 39 | 40 | test "request without credentials" do 41 | conn = call(TestPlug, []) 42 | assert_unauthorized conn, "Secret" 43 | end 44 | 45 | test "request with invalid user" do 46 | conn = call(TestPlug, [auth_header("Hacker:SecretPass")]) 47 | assert_unauthorized conn, "Secret" 48 | end 49 | 50 | test "request with invalid password" do 51 | conn = call(TestPlug, [auth_header("Admin:ASecretPass")]) 52 | assert_unauthorized conn, "Secret" 53 | end 54 | 55 | test "request with valid credentials" do 56 | conn = call(TestPlug, [auth_header("Admin:SecretPass")]) 57 | assert_authorized conn, "Authorized" 58 | end 59 | 60 | test "request with malformed credentials" do 61 | conn = call(TestPlug, [{"authorization", "Basic Zm9)"}]) 62 | assert_unauthorized conn, "Secret" 63 | end 64 | 65 | test "request with wrong scheme" do 66 | conn = call(TestPlug, [{"authorization", "Bearer #{Base.encode64("Admin:SecretPass")}"}]) 67 | assert_unauthorized conn, "Secret" 68 | end 69 | end -------------------------------------------------------------------------------- /lib/plug_auth/authentication/basic.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Authentication.Basic do 2 | @moduledoc """ 3 | Implements basic HTTP authentication. To use add: 4 | 5 | plug PlugAuth.Authentication.Basic, realm: "Secret world" 6 | 7 | to your pipeline. This module is derived from https://github.com/lexmag/blaguth 8 | """ 9 | 10 | @behaviour Plug 11 | import Plug.Conn 12 | import PlugAuth.Authentication.Utils 13 | 14 | @doc """ 15 | Add the credentials for a `user` and `password` combination. `user_data` can be any term but must not be `nil`. 16 | """ 17 | def add_credentials(user, password, user_data) do 18 | encode_creds(user, password) |> PlugAuth.CredentialStore.put_credentials(user_data) 19 | end 20 | 21 | @doc """ 22 | Remove the credentials for a `user` and `password` combination. 23 | """ 24 | def remove_credentials(user, password) do 25 | encode_creds(user, password) |> PlugAuth.CredentialStore.delete_credentials 26 | end 27 | 28 | @doc """ 29 | Changes the password for `user` from `old_password` to `new_password`. 30 | """ 31 | def update_credentials(user, old_password, new_password) do 32 | user_data = remove_credentials(user, old_password) 33 | add_credentials(user, new_password, user_data) 34 | end 35 | 36 | defp encode_creds(user, password), do: Base.encode64("#{user}:#{password}") 37 | 38 | def init(opts) do 39 | realm = Keyword.get(opts, :realm, "Restricted Area") 40 | error = Keyword.get(opts, :error, "HTTP Authentication Required") 41 | %{realm: realm, error: error} 42 | end 43 | 44 | def call(conn, opts) do 45 | conn 46 | |> get_auth_header 47 | |> verify_creds 48 | |> assert_creds(opts[:realm], opts[:error]) 49 | end 50 | 51 | defp get_auth_header(conn), do: {conn, get_first_req_header(conn, "authorization")} 52 | 53 | defp verify_creds({conn, << "Basic ", creds::binary >>}), do: {conn, PlugAuth.CredentialStore.get_user_data(creds)} 54 | defp verify_creds({conn, _}), do: {conn, nil} 55 | 56 | defp assert_creds({conn, nil}, realm, error), do: halt_with_login(conn, realm, error) 57 | defp assert_creds({conn, user_data}, _, _), do: assign_user_data(conn, user_data) 58 | 59 | defp halt_with_login(conn, realm, error) do 60 | conn 61 | |> put_resp_header("Www-Authenticate", ~s{Basic realm="#{realm}"}) 62 | |> halt_with_error(error) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/plug_auth/authentication/token_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Authentication.Token.Test do 2 | use ExUnit.Case, async: true 3 | use Plug.Test 4 | 5 | @error_msg ~s'{"error":"authentication required"}' 6 | 7 | defmodule ParamPlug do 8 | use Plug.Builder 9 | import Plug.Conn 10 | 11 | plug PlugAuth.Authentication.Token, source: :params, param: "auth_token", error: ~s'{"error":"authentication required"}' 12 | plug :index 13 | 14 | defp index(conn, _opts), do: send_resp(conn, 200, "Authorized") 15 | end 16 | 17 | defmodule HeaderPlug do 18 | use Plug.Builder 19 | import Plug.Conn 20 | 21 | plug PlugAuth.Authentication.Token, source: :header, param: "X-Auth-Token", error: ~s'{"error":"authentication required"}' 22 | plug :index 23 | 24 | defp index(conn, _opts), do: send_resp(conn, 200, "Authorized") 25 | end 26 | 27 | defp call(plug, params, headers) do 28 | conn(:get, "/", params, headers: headers) 29 | |> plug.call([]) 30 | end 31 | 32 | defp assert_unauthorized(conn, content) do 33 | assert conn.status == 401 34 | assert conn.resp_body == content 35 | refute conn.assigns[:authenticated_user] 36 | end 37 | 38 | defp assert_authorized(conn, content) do 39 | assert conn.status == 200 40 | assert conn.resp_body == content 41 | assert conn.assigns[:authenticated_user] == %{role: :admin} 42 | end 43 | 44 | defp auth_header(creds), do: {"X-Auth-Token", creds} 45 | defp auth_param(creds), do: {"auth_token", creds} 46 | 47 | setup do 48 | PlugAuth.Authentication.Token.add_credentials("secret_token", %{role: :admin}) 49 | end 50 | 51 | test "request without credentials using header-based auth" do 52 | conn = call(HeaderPlug, [], []) 53 | assert_unauthorized conn, @error_msg 54 | end 55 | 56 | test "request with invalid credentials using header-based auth" do 57 | conn = call(HeaderPlug, [], [auth_header("invalid_token")]) 58 | assert_unauthorized conn, @error_msg 59 | end 60 | 61 | test "request with valid credentials using header-based auth" do 62 | conn = call(HeaderPlug, [], [auth_header("secret_token")]) 63 | assert_authorized conn, "Authorized" 64 | end 65 | 66 | test "request without credentials using params-based auth" do 67 | conn = call(ParamPlug, [], []) 68 | assert_unauthorized conn, @error_msg 69 | end 70 | 71 | test "request with invalid credentials using params-based auth" do 72 | conn = call(ParamPlug, [auth_param("invalid_token")], []) 73 | assert_unauthorized conn, @error_msg 74 | end 75 | 76 | test "request with valid credentials using params-based auth" do 77 | conn = call(ParamPlug, [auth_param("secret_token")], []) 78 | assert_authorized conn, "Authorized" 79 | end 80 | end -------------------------------------------------------------------------------- /lib/plug_auth/authentication/database.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Authentication.Database do 2 | @moduledoc """ 3 | Implements Database authentication. To use add: 4 | 5 | plug PlugAuth.Authentication.Database, login: &MyController.login_callback/1 6 | 7 | to your pipeline. This module is derived from https://github.com/lexmag/blaguth 8 | """ 9 | 10 | @session_key Application.get_env(:plug_auth, :database_session_key, "database_auth") 11 | 12 | @behaviour Plug 13 | import Plug.Conn 14 | import PlugAuth.Authentication.Utils 15 | 16 | @doc """ 17 | Create a login for a user. `user_data` can be any term but must not be `nil`. 18 | """ 19 | def create_login(conn, user_data, id_key \\ :id) do 20 | id = UUID.uuid1 21 | id |> PlugAuth.CredentialStore.put_credentials(user_data, id_key) 22 | put_session(conn, @session_key, id) 23 | end 24 | 25 | @doc """ 26 | Delete a login. 27 | """ 28 | def delete_login(conn) do 29 | case get_session(conn, @session_key) do 30 | nil -> conn 31 | 32 | key -> 33 | PlugAuth.CredentialStore.delete_credentials(key) 34 | put_session(conn, @session_key, nil) 35 | |> put_session("user_return_to", nil) 36 | end 37 | |> delete_token_session 38 | end 39 | 40 | # @doc """ 41 | # Fetch user data from the credential store 42 | # """ 43 | # def get_user_data(conn) do 44 | # get_session(conn, @session_key) 45 | # |> PlugAuth.CredentialStore.get_user_data 46 | # end 47 | 48 | def init(opts) do 49 | error = Keyword.get(opts, :error, "HTTP Authentication Required") 50 | login = Keyword.get(opts, :login) 51 | db_model = Keyword.get(opts, :db_model) 52 | id_key = Keyword.get(opts, :id, :id) 53 | unless login do 54 | raise RuntimeError, message: "PlugAuth.Database requires a login redirect callback" 55 | end 56 | %{login: login, error: error, db_model: db_model, id_key: id_key} 57 | end 58 | 59 | def call(conn, opts) do 60 | unless get_authenticated_user(conn) do 61 | conn 62 | |> get_session_data 63 | |> verify_auth_key(opts) 64 | |> assert_login(opts[:login]) 65 | else 66 | conn 67 | end 68 | end 69 | 70 | defp get_session_data(conn) do 71 | {conn, get_session(conn, @session_key) } 72 | end 73 | 74 | defp verify_auth_key({conn, nil}, _), do: {conn, nil} 75 | defp verify_auth_key({conn, auth_key}, %{db_model: db_model, id_key: id_key}), 76 | do: {conn, PlugAuth.CredentialStore.get_user_data(auth_key, db_model, id_key)} 77 | 78 | defp assert_login({conn, nil}, login) do 79 | put_session(conn, "user_return_to", Path.join(["/" | conn.path_info])) 80 | |> login.() 81 | end 82 | defp assert_login({conn, user_data}, _), do: assign_user_data(conn, user_data) 83 | end 84 | -------------------------------------------------------------------------------- /lib/plug_auth/authentication/token.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugAuth.Authentication.Token do 2 | @moduledoc """ 3 | Implements token based authentication. To use add 4 | 5 | plug PlugAuth.Authentication.Token, source: :params, param: "auth_token" 6 | 7 | or 8 | 9 | plug PlugAuth.Authentication.Token, source: :session, param: "auth_token" 10 | 11 | or 12 | 13 | plug PlugAuth.Authentication.Token, source: :header, param: "X-Auth-Token" 14 | 15 | or 16 | 17 | plug PlugAuth.Authentication.Token, source: { module, function, ["my_param"]} end 18 | 19 | or 20 | 21 | plug PlugAuth.Authentication.Token, source: :params_session, param: "auth_token" 22 | 23 | to your pipeline. 24 | """ 25 | 26 | @behaviour Plug 27 | import Plug.Conn 28 | import PlugAuth.Authentication.Utils 29 | require Logger 30 | 31 | @doc """ 32 | Add the credentials for a `token`. `user_data` can be any term but must not be `nil`. 33 | """ 34 | def add_credentials(token, user_data) do 35 | PlugAuth.CredentialStore.put_credentials(token, user_data) 36 | end 37 | 38 | @doc """ 39 | Remove the credentials for a `token`. 40 | """ 41 | def remove_credentials(token) do 42 | PlugAuth.CredentialStore.delete_credentials(token) 43 | end 44 | 45 | @doc """ 46 | Utility function to generate a random authentication token. 47 | """ 48 | def generate_token() do 49 | :crypto.strong_rand_bytes(16) |> Base.url_encode64 50 | end 51 | 52 | def init(opts) do 53 | param = Keyword.get(opts, :param) 54 | source = Keyword.fetch!(opts, :source) |> convert_source(param) 55 | error = Keyword.get(opts, :error, "HTTP Authentication Required") 56 | %{source: source, error: error} 57 | end 58 | 59 | defp convert_source(:params_session, param), do: {__MODULE__, :get_token_from_params_session, [param]} 60 | defp convert_source(:params, param), do: {__MODULE__, :get_token_from_params, [param]} 61 | defp convert_source(:header, param), do: {__MODULE__, :get_token_from_header, [param]} 62 | defp convert_source(:session, param), do: {__MODULE__, :get_token_from_session, [param]} 63 | defp convert_source(source = {module, fun, args}, _param) when is_atom(module) and is_atom(fun) and is_list(args), do: source 64 | 65 | def get_token_from_params(conn, param), do: {conn, conn.params[param]} 66 | def get_token_from_header(conn, param), do: {conn, get_first_req_header(conn, param)} 67 | def get_token_from_session(conn, param), do: {conn, get_session(conn, param)} 68 | 69 | def get_token_from_params_session(conn, param) do 70 | get_token_from_params(conn, param) 71 | |> check_token_from_session(param) 72 | |> save_token_in_session(param) 73 | end 74 | def check_token_from_session({conn, nil}, param), do: get_token_from_session(conn, param) 75 | def check_token_from_session({conn, creds}, _param), do: {conn, creds} 76 | 77 | def save_token_in_session({conn, nil}, _), do: {conn, nil} 78 | def save_token_in_session({conn, creds}, param) do 79 | {put_session(conn, param, creds) |> put_session(param_key, param), creds} 80 | end 81 | 82 | def call(conn, opts) do 83 | unless get_authenticated_user(conn) do 84 | {module, fun, args} = opts[:source] 85 | apply(module, fun, [conn | args]) 86 | |> verify_creds 87 | |> assert_creds(opts[:error]) 88 | else 89 | conn 90 | end 91 | end 92 | 93 | defp verify_creds({conn, creds}), do: {conn, PlugAuth.CredentialStore.get_user_data(creds)} 94 | 95 | defp assert_creds({conn, nil}, nil), do: conn 96 | defp assert_creds({conn, nil}, error), do: halt_with_error(conn, error) 97 | defp assert_creds({conn, user_data}, _), do: assign_user_data(conn, user_data) 98 | end 99 | 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlugAuth 2 | 3 | PlugAuth is a collection of authentication-related plugs. It currently performs two tasks: 4 | 5 | * Authentication 6 | * Access control 7 | 8 | ## Usage 9 | 10 | Add PlugAuth as a dependency in your `mix.exs` file. 11 | 12 | ```elixir 13 | defp deps do 14 | [{:plug_auth, ">= 0.0.0"}] 15 | end 16 | ``` 17 | 18 | You should also update your applications list to include a webserver (e.g. cowboy), plug and plug_auth: 19 | 20 | ```elixir 21 | def application do 22 | [applications: [:cowboy, :plug, :plug_auth]] 23 | end 24 | ``` 25 | 26 | After you are done, run `mix deps.get` in your shell to fetch the dependencies. 27 | 28 | ## Authentication 29 | 30 | Currently three authentication methods are supported: HTTP Basic, Token-based, and Database-based. In either case you will first have to add valid credentials in the credential store. Multiple credentials can be added. The plugs provide convenience methods for this task. 31 | 32 | ### HTTP Basic Example 33 | ```elixir 34 | PlugAuth.Authentication.Basic.add_credentials("Admin", "SecretPass", %{role: :admin}) 35 | ``` 36 | 37 | ### Token Example 38 | ```elixir 39 | token = PlugAuth.Authentication.Token.generate_token 40 | PlugAuth.Authentication.Token.add_credentials(token, %{role: :admin}) 41 | ``` 42 | 43 | ### Database Example 44 | ```elixir 45 | id = params["id"] 46 | user = Repo.one(from u in User, where: u.id == ^id, preload: [:roles] 47 | PlugAuth.Authentication.Database.create_login(conn, user) 48 | ``` 49 | 50 | The last argument in both cases can be any term, except nil. On successful authentication it will be stored by the authentication plug in the assign map of the connection with the :authenticated_user atom as key. You can retrieve it using 51 | 52 | ```elixir 53 | PlugAuth.Authentication.Utils.get_authenticated_user(conn) 54 | ``` 55 | 56 | The content of this term is not used for authentication purposes, but can be useful to store application specific information about the user (for example, the user id, its role, etc). 57 | 58 | To perform authentication, you can add either plug to your pipeline. 59 | 60 | ### HTTP Basic Example 61 | ```elixir 62 | plug PlugAuth.Authentication.Basic, realm: "Secret" 63 | ``` 64 | The realm parameter is optional and can be omitted. By default "Restricted Area" will be used as realm name. You can also pass the error parameter, which should be a string and will be sent instead of the default message "HTTP Authentication Required" on authentication failure (with status code 401). 65 | 66 | ### Token Example 67 | ```elixir 68 | plug PlugAuth.Authentication.Token, source: :params, param: "auth_token", error: ~s'{"error":"authentication required"}' 69 | ``` 70 | The error parameter is optional and is treated as in the example above. The source parameter defines how to retrieve the token from the connection. Currently, the three acceptable values are: :params, :header and :session. Their name is self-explainatory. The param parameter defines the name of the parameter/HTTP header/session key where the token is stored. This should cover most cases, but if retrieving the token is more complex than that, you can pass a tuple for the source parameter. The tuple must be in the form `{MyModule, :my_function, ["param1", 42]}`. The function must accept a connection as its first argument (which will be injected as the head of the given parameter list) and any other number of parameters, which must be given in the third element of the tuple. If no additional arguments are needed, an empty list must be given. 71 | 72 | ### Database Example 73 | ```elixir 74 | plug PlugAuth.Authentication.Database, login: &Auth.SessionController.login_callback/1 75 | ``` 76 | 77 | where login_callback will render the login page. In phoenix, this would look like: 78 | 79 | ```elixir 80 | def login_callback(conn) do 81 | conn 82 | |> put_layout({Auth.LayoutView, "auth.html"}) 83 | |> put_view(Auth.SessionView) 84 | |> render("new.html") 85 | |> halt 86 | end 87 | ``` 88 | 89 | ### Combining Token and Database Authentication together 90 | ```elixir 91 | plug PlugAuth.Authentication.Token, source: :params_session, param: "auth_token", error: nil 92 | plug PlugAuth.Authentication.Database, login: &Auth.SessionController.login_callback/1 93 | ``` 94 | 95 | Note: setting error: nil on PlugAuth.Authentication.Token skips the error association, allowing it to fall through to the Database authentication. 96 | 97 | ## Access control 98 | PlugAuth currently provides role-based access control, which can be performed after authentication. You would use it like this 99 | 100 | ```elixir 101 | plug PlugAuth.Authentication.Basic, realm: "Secret" 102 | plug PlugAuth.Access.Role, roles: [:admin, :developer] 103 | ``` 104 | 105 | In the example above HTTP basic authentication is used, but you could use any other authentication plug as well. The roles parameter specifies which user roles are granted access. On authentication failure the HTTP status code 403 will be sent, together with an error message which can be set using the error parameter (just like in the Authentication examples). 106 | 107 | The role of the currently authenticated user, is read from the :authenticated_user assign of the connection. If when adding credentials you passed a map or strucutre as the user data and this map has a "role" key, then everything will work automatically. If your user data is not a map or a structure, or it does not contain the role key, you can implemented the ```PlugAuth.Access.RoleAdapter``` protocol instead. 108 | 109 | ## License 110 | Copyright (c) 2014, Michele Balistreri 111 | 112 | Permission to use, copy, modify, and/or distribute this software for any 113 | purpose with or without fee is hereby granted, provided that the above 114 | copyright notice and this permission notice appear in all copies. 115 | 116 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 117 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 118 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 119 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 120 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 121 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 122 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 123 | --------------------------------------------------------------------------------