Current section
Files
Jump to
Current section
Files
lib/plug_hyper_auth.ex
# This file is part of PlugHyperAuth.
# Copyright (C) 2017 Jesús Hernández Gormaz
#
# PlugHyperAuth is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# PlugHyperAuth is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
defmodule PlugHyperAuth do
import Plug.Conn
@moduledoc """
Plug for HTTP AAA using the HTTP auth framework.
When a configured scheme is found in the authorization header
it will use that to process the values of the header (with
access to the connection but the modifications are ignored),
with that values and other generic values the authenticator
will authenticate the user (without access to connection).
This plug is extensible with the behaviours:
* `PlugHyperAuth.Scheme`
* `HyperAuth.Authenticator`
This library support the common schemes then often you only need
extend it with modules of `HyperAuth.Authenticator` behaviour
(more safe than extend the schemes) like.
The behaviour of `PlugHyperAuth` is showed in the next table where:
* **TLS**: If the connection is secure (HTTPS, HTTP over SSL/TLS).
* **Public**: If the resource is configured as public access allowed.
* **Header**: If exists the authorization header in the request.
* **Auth**: If the credentials are valid.
* **Status**: The HTTP status code response.
* **User**: The user authenticated.
| TLS | Public | Header | Auth | Status | User |
|-----|--------|--------|------|--------|------|
| NO | NO | NO | NO | 403 | anon |
| NO | NO | YES | NO | 403 | anon |
| NO | NO | YES | YES | 403 | anon |
| NO | YES | NO | NO | 200 | anon |
| NO | YES | YES | NO | 403 | anon |
| NO | YES | YES | YES | 403 | anon |
| YES | NO | NO | NO | 401 | anon |
| YES | NO | YES | NO | 401 | anon |
| YES | YES | NO | NO | 200 | anon |
| YES | YES | YES | NO | 200 | anon |
| YES | NO | YES | YES | 200 | user |
| YES | YES | YES | YES | 200 | user |
"""
@doc """
Configure the schemes alloweds and the authenticator.
The default schemes are:
* Basic: PlugHyperAuth.Scheme.Basic
* Digest: PlugHyperAuth.Scheme.Digest
"""
def init(opts) do
schemes = Keyword.get(opts, :schemes, %{})
|> Map.put_new("basic", __MODULE__.Scheme.Basic)
|> Map.put_new("digest", __MODULE__.Scheme.Digest)
Keyword.put(opts, :schemes, schemes)
end
def call(conn, opts) do
authorization = conn
|> get_req_header("authorization")
|> List.first
cond do
# Allow public without authorization
opts[:public] && is_nil(authorization) ->
conn
# Forbidden authorization for not public over HTTP
# Auth only allowed over secure connection
conn.scheme != :https && conn.remote_ip != {127, 0, 0, 1} ->
forbidden conn, opts
# Check if not exists authorization
is_nil(authorization) || String.length(authorization) == 0 ->
unauthorized conn, opts
# Limit the authorization size
String.length(authorization) > 4096 ->
header_fields_too_large conn, opts
true ->
# Parse authorization header value
case __MODULE__.HTTP.parse_authorization authorization do
{scheme, tokens, authorization_properties} ->
# Process authorization credentials
credentials = process_authorization conn, scheme, tokens, authorization_properties, opts
if is_map credentials do
# Authenticate
user = HyperAuth.authenticate credentials, opts
if is_map user do
# Put the user in the connection
put_private conn, :auth_user, credentials
else
unauthorized conn, opts
end
else
unauthorized conn, opts
end
_ ->
unauthorized conn, opts
end
end
end
defp process_authorization(conn, scheme, tokens, authorization_properties, opts) do
# Call the scheme module
scheme_module = opts[:schemes][scheme]
if is_nil scheme_module do
nil
else
scheme_module.process_authorization conn, tokens, authorization_properties, opts
end
end
defp request_authenticate(conn, opts) do
opts[:schemes]
|> Map.values()
|> Enum.map(fn(scheme_module) ->
# Call the scheme module
scheme_module.request_authenticate(conn, opts)
end)
|> Enum.join(" ")
end
defp unauthorized(conn, opts) do
if opts[:public] do
conn
else
authorization_request = request_authenticate conn, opts
conn
|> put_resp_header("www-authenticate", authorization_request)
|> send_resp(:unauthorized, "unauthorized")
|> halt()
end
end
defp forbidden(conn, _opts) do
conn
|> send_resp(:forbidden, "forbidden")
|> halt()
end
defp header_fields_too_large(conn, _opts) do
conn
|> send_resp(:request_header_fields_too_large, "request header fields too large")
|> halt()
end
end