项目作者: Ianleeclark

项目描述 :
A server-side U2F (Universal Second Factor) library in Elixir
高级语言: Elixir
项目地址: git://github.com/Ianleeclark/u2f_ex.git
创建时间: 2018-08-08T01:28:54Z
项目社区:https://github.com/Ianleeclark/u2f_ex

开源协议:BSD 3-Clause "New" or "Revised" License

下载


U2fEx

CircleCI
Hex.pm
HexDocs

A Pure Elixir implementation of the U2F Protocol.

Installation

If available in Hex, the package can be installed
by adding u2f_ex to your list of dependencies in mix.exs:

  1. def deps do
  2. [
  3. {:u2f_ex, "~> 0.4.2"}
  4. ]
  5. end

PKIStorage

In order to properly use this library, you’re going to need to store metadata and public
keys for any user registering their U2F Token. However, u2f_ex will need to retrieve that
metadata, so you’re get to write a glorious new module implementing our storage behaviour.

Check out some example docs here: PKIStorage Example

Add A New SQL Table

This section assumes that you’ll be using SQL as the primary storage mechanism for these keys,
but, if you plan on using something else, feel free to do so! Skip to the next section and, should
you have any questions, feel free to ask!
First you’ll want to create a model capable of representing the key metadata (you can steal the
following code):

  1. defmodule Example.Users.U2FKey do
  2. use Ecto.Schema
  3. import Ecto.Changeset
  4. alias Example.Users.User
  5. schema "u2f_keys" do
  6. field(:public_key, :string, size: 128, null: false)
  7. field(:key_handle, :string, size: 128, null: false)
  8. field(:version, :string, size: 10, null: false, default: "U2F_V2")
  9. field(:app_id, :string, null: false)
  10. # NOTE: You'll need to update what table this references or change it to a normal field
  11. belongs_to(:user, User)
  12. timestamps()
  13. end
  14. @doc false
  15. def changeset(user, attrs) do
  16. user
  17. |> cast(attrs, [:public_key, :key_handle, :version, :app_id, :user_id])
  18. |> validate_required([:public_key, :key_handle, :version, :app_id, :user_id])
  19. |> validate_b64_string(:public_key)
  20. |> validate_b64_string(:key_handle)
  21. end
  22. @doc false
  23. def validate_b64_string(changeset, field, opts \\ []) do
  24. validate_change(changeset, field, fn _, value ->
  25. case Base.decode64(value, padding: false) do
  26. {:ok, _result} ->
  27. []
  28. _ ->
  29. [{field, opts[:message] || "Invalid field #{field}. Expected b64 encoded string."}]
  30. end
  31. end)
  32. end
  33. end

Finally, create and run the following migration:

  1. defmodule Example.Repo.Migrations.AddU2fKey do
  2. use Ecto.Migration
  3. def change do
  4. create table(:u2f_keys) do
  5. add(:public_key, :string, size: 128)
  6. add(:key_handle, :string, size: 128)
  7. add(:version, :string, size: 10, default: "U2F_V2")
  8. add(:app_id, :string)
  9. # NOTE: You'll need to update what table this references or change it to a normal field
  10. add(:user_id, references(:users))
  11. timestamps()
  12. end
  13. end
  14. end

Create a PKIStorage Module

Next you’ll need to provide the library a way of storing and fetching metadata about stored U2F keys,
so you’ll implement the Storage Behaviour

An example, that uses Ecto + SQL, will follow, but know that you can use whatever storage mechanism you
want so long as you adhere to the contract.

  1. defmodule Example.PKIStorage do
  2. @moduledoc false
  3. import Ecto.Query
  4. alias Example.Repo
  5. alias U2FEx.PKIStorageBehaviour
  6. alias Example.Users.U2FKey
  7. @behaviour U2FEx.PKIStorageBehaviour
  8. @impl PKIStorageBehaviour
  9. def list_key_handles_for_user(user_id) do
  10. q =
  11. from(u in U2FKey,
  12. where: u.user_id == ^user_id
  13. )
  14. x =
  15. q
  16. |> Repo.all()
  17. |> Enum.map(fn %U2FKey{version: version, key_handle: key_handle, app_id: app_id} ->
  18. %{version: version, key_handle: key_handle, app_id: app_id}
  19. end)
  20. {:ok, x}
  21. end
  22. @impl PKIStorageBehaviour
  23. def get_public_key_for_user(user_id, key_handle) do
  24. q = from(u in U2FKey, where: u.user_id == ^user_id and u.key_handle == ^key_handle)
  25. q
  26. |> Repo.one()
  27. |> case do
  28. nil -> {:error, :public_key_not_found}
  29. %U2FKey{public_key: public_key} -> {:ok, public_key}
  30. end
  31. end
  32. end

Config Value

Next you’ll need to update your configuration to set the PKIStorage model:

  1. config :u2f_ex,
  2. pki_storage: PKIStorage,
  3. app_id: "https://yoursite.com"
NOTE: The should be your site.

Create a Controller

You’ll need a controller capable of handling these interactions:

  1. defmodule ExampleWeb.U2FController do
  2. use ExampleWeb, :controller
  3. alias Example.Users
  4. alias Example.Users.U2FKey
  5. alias U2FEx.KeyMetadata
  6. @doc """
  7. This is the first interaction in the u2f flow. We'll challenge the u2f token to
  8. provide a public key and sign our challenge (+ other info) proving their ownership
  9. of the corresponding private key.
  10. """
  11. def start_registration(conn, _params) do
  12. with {:ok, registration_data} <- U2FEx.start_registration(get_user_id(conn)) do
  13. output = %{
  14. registerRequests: [
  15. %{
  16. appId: registration_data.appId,
  17. padding: false,
  18. version: "U2F_V2",
  19. challenge: registration_data.challenge,
  20. padding: false
  21. }
  22. ],
  23. registeredKeys: []
  24. }
  25. conn
  26. |> json(output)
  27. end
  28. end
  29. @doc """
  30. This is the second step of the registration where we'll store their key metadata for
  31. use later in the authentication portion of the flow.
  32. """
  33. def finish_registration(conn, device_response) do
  34. user_id = get_user_id(conn)
  35. with {:ok, %KeyMetadata{} = key_metadata} <-
  36. U2FEx.finish_registration(user_id, device_response),
  37. :ok <- store_key_data(user_id, key_metadata) do
  38. conn
  39. |> json(%{"success" => true})
  40. else
  41. _error ->
  42. conn |> put_status(:bad_request) |> json(%{"success" => false})
  43. end
  44. end
  45. @doc """
  46. Should the user be logging in, and they have a u2f key registered in our system, we
  47. should challenge that user to prove their identity and ownership of the u2f device.
  48. """
  49. def start_authentication(conn, _params) do
  50. with {:ok, %{} = sign_request} <- U2FEx.start_authentication(get_user_id(conn)) do
  51. conn
  52. |> json(sign_request)
  53. end
  54. end
  55. @doc """
  56. After the user has attempted to verify their identity, U2FEx will verify they actually who are
  57. they say they are. Once this step has exited successfully, then we can be reasonably assured the
  58. user is who they claim to be.
  59. """
  60. def finish_authentication(conn, device_response) do
  61. with :ok <- U2FEx.finish_authentication(get_user_id(conn), device_response |> Jason.encode!()) do
  62. conn
  63. |> json(%{"success" => true})
  64. else
  65. _ -> json(conn, %{"success" => false})
  66. end
  67. end
  68. @doc """
  69. Fill in with however you want to persist keys. See U2FEx.KeyMetadata struct for more info
  70. """
  71. @spec store_key_data(user_id :: any(), KeyMetadata.t()) :: :ok | {:error, any()}
  72. def store_key_data(user_id, key_metadata) do
  73. with {:ok, %U2FKey{}} <- Users.create_u2f_key(user_id, key_metadata) do
  74. :ok
  75. end
  76. end
  77. @spec get_user_id(Plug.Conn.t()) :: String.t()
  78. defp get_user_id(_conn) do
  79. "1"
  80. end
  81. end

Moreover, you’re going to need to add routes (feel free to change, but you need these four routes specifically).

  1. post("/u2f/start_registration", U2FController, :start_registration)
  2. post("/u2f/finish_registration", U2FController, :finish_registration)
  3. post("/u2f/start_authentication", U2FController, :start_authentication)
  4. post("/u2f/finish_authentication", U2FController, :finish_authentication)

Finally, finish up with some javascript

Vendor google’s u2f-api-polyfill.js (Can be found here or here).

Finally, you’ll need to handle events for talking to the device. This assumes jquery, but it can be
easily swapped out and work in vanilla Javascript, React, Vue, &c.

  1. import $ from "jquery";
  2. $(document).ready(() => {
  3. const appId = "https://localhost";
  4. const u2f = window.u2f;
  5. const post = (url, csrf, data) => {
  6. return $.ajax({
  7. url: url,
  8. type: "POST",
  9. dataType: "json",
  10. contentType: "application/json",
  11. data: JSON.stringify(data),
  12. beforeSend: xhr => {
  13. xhr.setRequestHeader("X-CSRF-TOKEN", csrf);
  14. }
  15. });
  16. };
  17. $("#register").click(() => {
  18. const csrf = $("meta[name='csrf-token']").attr("content");
  19. post("/u2f/start_registration", csrf).then(
  20. ({ appId, registerRequests, registeredKeys }) => {
  21. u2f.register(appId, registerRequests, registeredKeys, response => {
  22. post("/u2f/finish_registration", csrf, response)
  23. // NOTE: Handle finishing registration here
  24. .then(x => console.log("Finished Registration"));
  25. });
  26. },
  27. error => {
  28. console.error(error);
  29. }
  30. );
  31. });
  32. $("#sign").click(() => {
  33. const csrf = $("meta[name='csrf-token']").attr("content");
  34. post("/u2f/start_authentication", csrf).then(
  35. ({ challenge, registeredKeys }) => {
  36. u2f
  37. .sign(appId, challenge, registeredKeys, response1 => {
  38. post("/u2f/finish_authentication", csrf, response1).then(
  39. // NOTE: Handle finishing authentication here
  40. x => console.log("Finished Authentication")
  41. );
  42. });
  43. },
  44. error => {
  45. console.error(error);
  46. }
  47. );
  48. });
  49. });