Repository: phoenixframework/phoenix_html
Pull request #166 (2017-07-25 17:08:02)

Allow specifying a custom CSRF token generator
**Work In Progress**

The current implementation is tied up to Plug's CSRF protection code. We need some flexibility around the form-related helpers to, for example, generate a CSRF token input compatible with other frameworks or generate a blank token, so the final HTML output is cacheable.

#### TODO

* [x] Docs
* [x] Tests
* [x] Refactor


Patch:
diff --git a/lib/phoenix_html/link.ex b/lib/phoenix_html/link.ex
index 43db8c8..200643d 100644
--- a/lib/phoenix_html/link.ex
+++ b/lib/phoenix_html/link.ex
@@ -57,6 +57,14 @@ defmodule Phoenix.HTML.Link do
   or use the above data attributes, `Phoenix.HTML` relies
   on JavaScript. You can load `priv/static/phoenix_html.js`
   into your build tool.
+
+  ## CSRF Protection
+
+  By default, CSRF tokens are generated through `Plug.CSRFProtection`. You
+  can customize the CSRF token generation by configuring your own MFA:
+
+      config :phoenix_html, csrf_token_generator: {MyGenerator, :get_token, []}
+
   """
   def link(text, opts)
 
@@ -135,13 +143,18 @@ defmodule Phoenix.HTML.Link do
 
   defp csrf_data(opts) do
     {csrf_token?, opts} = Keyword.pop(opts, :csrf_token, true)
-    if csrf_token = csrf_token? && Plug.CSRFProtection.get_csrf_token() do
+    if csrf_token = csrf_token? && get_csrf_token() do
       {[csrf: csrf_token], opts}
     else
       {[], opts}
     end
   end
 
+  defp get_csrf_token do
+    {mod, fun, args} = Application.fetch_env!(:phoenix_html, :csrf_token_generator)
+    apply(mod, fun, args)
+  end
+
   defp extract_button_options(opts) do
     {to, opts} = pop_required_option!(opts, :to, "option :to is required in button/2")
     {method, opts} = Keyword.pop(opts, :method, :post)
diff --git a/lib/phoenix_html/tag.ex b/lib/phoenix_html/tag.ex
index 1356ca4..a25c9a3 100644
--- a/lib/phoenix_html/tag.ex
+++ b/lib/phoenix_html/tag.ex
@@ -7,7 +7,6 @@ defmodule Phoenix.HTML.Tag do
   """
 
   import Phoenix.HTML
-  import Plug.CSRFProtection, only: [get_csrf_token: 0]
 
   @tag_prefixes [:aria, :data]
   @csrf_param "_csrf_token"
@@ -184,6 +183,14 @@ defmodule Phoenix.HTML.Tag do
   in your forms, to force those browsers to send the data in the proper
   encoding. This technique has been seen in the Rails web framework and
   reproduced here.
+
+  ## CSRF Protection
+
+  By default, CSRF tokens are generated through `Plug.CSRFProtection`. You
+  can customize the CSRF token generation by configuring your own MFA:
+
+      config :phoenix_html, csrf_token_generator: {MyGenerator, :get_token, []}
+
   """
   def form_tag(action, opts \\ [])
 
@@ -257,6 +264,11 @@ defmodule Phoenix.HTML.Tag do
                'csrf-param': @csrf_param, 'method-param': @method_param
   end
 
+  defp get_csrf_token do
+    {mod, fun, args} = Application.fetch_env!(:phoenix_html, :csrf_token_generator)
+    apply(mod, fun, args)
+  end
+
   @doc """
   Generates an img tag with a src.
 
diff --git a/mix.exs b/mix.exs
index 89f5a88..0b614d5 100644
--- a/mix.exs
+++ b/mix.exs
@@ -18,7 +18,8 @@ defmodule PhoenixHtml.Mixfile do
   end
 
   def application do
-    [applications: [:logger, :plug]]
+    [applications: [:logger, :plug],
+     env: [csrf_token_generator: {Plug.CSRFProtection, :get_csrf_token, []}]]
   end
 
   defp deps do


Test patch:
diff --git a/test/phoenix_html/csrf_test.exs b/test/phoenix_html/csrf_test.exs
new file mode 100644
index 0000000..e2f8d4a
--- /dev/null
+++ b/test/phoenix_html/csrf_test.exs
@@ -0,0 +1,58 @@
+defmodule Phoenix.HTML.CsrfTest do
+  use ExUnit.Case, async: false
+
+  import Phoenix.HTML
+  import Phoenix.HTML.Link
+  import Phoenix.HTML.Tag
+
+  defmodule TokenGenerator do
+    def get, do: "custom-token"
+  end
+
+  setup_all do
+    previous_generator = Application.fetch_env!(:phoenix_html, :csrf_token_generator)
+
+    Application.put_env(:phoenix_html, :csrf_token_generator, {TokenGenerator, :get, []})
+
+    on_exit fn ->
+      Application.put_env(:phoenix_html, :csrf_token_generator, previous_generator)
+    end
+
+    :ok
+  end
+
+  test "link with post using a custom csrf token" do
+    assert safe_to_string(link("hello", to: "/world", method: :post)) ==
+           ~s[<a data-csrf="custom-token" data-method="post" data-to="/world" href="#" rel="nofollow">hello</a>]
+  end
+
+  test "link with put/delete using a custom csrf token" do
+    assert safe_to_string(link("hello", to: "/world", method: :put)) ==
+           ~s[<a data-csrf="custom-token" data-method="put" data-to="/world" href="#" rel="nofollow">hello</a>]
+  end
+
+  test "button with post using a custom csrf token" do
+    assert safe_to_string(button("hello", to: "/world")) ==
+          ~s[<button data-csrf="custom-token" data-method="post" data-to="/world">hello</button>]
+  end
+
+  test "form_tag for post using a custom csrf token" do
+    assert safe_to_string(form_tag("/")) ==
+           ~s(<form accept-charset="UTF-8" action="/" method="post">) <>
+           ~s(<input name="_csrf_token" type="hidden" value="custom-token">) <>
+           ~s(<input name="_utf8" type="hidden" value="✓">)
+  end
+
+  test "form_tag for other method using a custom csrf token" do
+    assert safe_to_string(form_tag("/", method: :put)) ==
+           ~s(<form accept-charset="UTF-8" action="/" method="post">) <>
+           ~s(<input name="_method" type="hidden" value="put">) <>
+           ~s(<input name="_csrf_token" type="hidden" value="custom-token">) <>
+           ~s(<input name="_utf8" type="hidden" value="✓">)
+  end
+
+  test "csrf_meta_tag" do
+    assert safe_to_string(csrf_meta_tag()) ==
+           ~s(<meta charset="UTF-8" content="custom-token" csrf-param="_csrf_token" method-param="_method" name="csrf-token">)
+  end
+end