diff --git a/README.md b/README.md index 1ff72c1..d2532a7 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,9 @@ defmodule SampleAppWeb.Endpoint do @client_id Application.compile_env!(:sample_app, [:openid_credentials, :client_id]) @client_secret Application.compile_env!(:sample_app, [:openid_credentials, :client_secret]) + # Ensure Authorization Token provided + plug Oidcc.Plug.RequireAuthorization + # Check Token via Introspection plug Oidcc.Plug.IntrospectToken, provider: SampleApp.GoogleOpenIdConfigurationProvider, diff --git a/lib/oidcc/plug/extract_authorization.ex b/lib/oidcc/plug/extract_authorization.ex index b792bf9..e84adab 100644 --- a/lib/oidcc/plug/extract_authorization.ex +++ b/lib/oidcc/plug/extract_authorization.ex @@ -13,6 +13,7 @@ defmodule Oidcc.Plug.ExtractAuthorization do plug Oidcc.Plug.ExtractAuthorization + plug Oidcc.Plug.RequireAuthorization, [...] # Ensure Authorization Token provided plug Oidcc.Plug.IntrospectToken, [...] # Check Token via Introspection plug Oidcc.Plug.LoadUserinfo, [...] # Check Token via Userinfo plug Oidcc.Plug.ValidateJwtToken, [...] # Check Token via JWT validation diff --git a/lib/oidcc/plug/require_authorization.ex b/lib/oidcc/plug/require_authorization.ex new file mode 100644 index 0000000..eafe66d --- /dev/null +++ b/lib/oidcc/plug/require_authorization.ex @@ -0,0 +1,73 @@ +defmodule Oidcc.Plug.RequireAuthorization do + @moduledoc """ + Ensure authorization token provided. + + This module should be used together with `Oidcc.Plug.ExtractAuthorization`. + + ```elixir + defmodule SampleAppWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :sample_app + + # ... + + plug Oidcc.Plug.ExtractAuthorization + + plug Oidcc.Plug.RequireAuthorization + + # Check Token with `Oidcc.Plug.IntrospectToken`, `Oidcc.Plug.LoadUserinfo` or `Oidcc.Plug.ValidateJwtToken` + + plug SampleAppWeb.Router + end + ``` + """ + @moduledoc since: "0.1.0" + + @behaviour Plug + + import Plug.Conn, only: [halt: 1, send_resp: 3, put_resp_header: 3] + + alias Oidcc.Plug.ExtractAuthorization + + @typedoc """ + Plug Configuration Options + + ## Options + + * `send_missing_token_response` - Customize Error Response for missing token + """ + @typedoc since: "0.1.0" + @type opts :: [ + send_missing_token_response: (conn :: Plug.Conn.t() -> Plug.Conn.t()) + ] + + @impl Plug + def init(opts), + do: + Keyword.validate!(opts, + send_missing_token_response: &__MODULE__.send_missing_token_response/1 + ) + + @impl Plug + def call(%Plug.Conn{private: %{ExtractAuthorization => nil}} = conn, opts) do + send_missing_token_response = Keyword.fetch!(opts, :send_missing_token_response) + + send_missing_token_response.(conn) + end + + def call(%Plug.Conn{private: %{ExtractAuthorization => _access_token}} = conn, _opts), do: conn + + def call(%Plug.Conn{} = _conn, _opts) do + raise """ + The plug Oidcc.Plug.ExtractAuthorization must be run before this plug + """ + end + + @doc false + @spec send_missing_token_response(conn :: Plug.Conn.t()) :: Plug.Conn.t() + def send_missing_token_response(conn) do + conn + |> halt() + |> put_resp_header("www-authenticate", "Bearer") + |> send_resp(:unauthorized, "The authorization token is required") + end +end diff --git a/test/oidcc/plug/require_authorization.exs b/test/oidcc/plug/require_authorization.exs new file mode 100644 index 0000000..4efe665 --- /dev/null +++ b/test/oidcc/plug/require_authorization.exs @@ -0,0 +1,51 @@ +defmodule Oidcc.Plug.RequireAuthorizationTest do + use ExUnit.Case, async: false + use Plug.Test + + alias Oidcc.Plug.ExtractAuthorization + alias Oidcc.Plug.RequireAuthorization + + doctest RequireAuthorization + + describe inspect(&RequireAuthorization.call/2) do + test "errors without ExtractAuthorization" do + opts = + RequireAuthorization.init([]) + + assert_raise RuntimeError, fn -> + "get" + |> conn("/", "") + |> RequireAuthorization.call(opts) + end + end + + test "send error response if no token provided" do + opts = + RequireAuthorization.init([]) + + assert %{ + halted: true, + status: 401, + resp_headers: [ + {"cache-control", "max-age=0, private, must-revalidate"}, + {"www-authenticate", "Bearer"} + ] + } = + "get" + |> conn("/", "") + |> put_private(ExtractAuthorization, nil) + |> RequireAuthorization.call(opts) + end + + test "pass if token provided" do + opts = + RequireAuthorization.init([]) + + assert %{halted: false} = + "get" + |> conn("/", "") + |> put_private(ExtractAuthorization, "some_access_token") + |> RequireAuthorization.call(opts) + end + end +end