{"id":28388867,"url":"https://github.com/ianleeclark/u2f_ex","last_synced_at":"2025-12-11T23:42:38.926Z","repository":{"id":48290296,"uuid":"143945078","full_name":"Ianleeclark/u2f_ex","owner":"Ianleeclark","description":"A server-side U2F (Universal Second Factor) library in Elixir","archived":false,"fork":false,"pushed_at":"2023-01-04T18:49:07.000Z","size":373,"stargazers_count":25,"open_issues_count":9,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-13T13:48:32.018Z","etag":null,"topics":["elixir","u2f","u2f-server"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Ianleeclark.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-08-08T01:28:54.000Z","updated_at":"2023-08-23T11:19:46.000Z","dependencies_parsed_at":"2023-02-02T20:00:37.943Z","dependency_job_id":null,"html_url":"https://github.com/Ianleeclark/u2f_ex","commit_stats":null,"previous_names":["grappigpanda/u2f_ex"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/Ianleeclark/u2f_ex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ianleeclark%2Fu2f_ex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ianleeclark%2Fu2f_ex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ianleeclark%2Fu2f_ex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ianleeclark%2Fu2f_ex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Ianleeclark","download_url":"https://codeload.github.com/Ianleeclark/u2f_ex/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ianleeclark%2Fu2f_ex/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268185560,"owners_count":24209392,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-01T02:00:08.611Z","response_time":67,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir","u2f","u2f-server"],"created_at":"2025-05-30T23:16:24.859Z","updated_at":"2025-12-11T23:42:38.886Z","avatar_url":"https://github.com/Ianleeclark.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# U2fEx\n[![CircleCI](https://circleci.com/gh/GrappigPanda/u2f_ex/tree/master.svg?style=svg)](https://circleci.com/gh/GrappigPanda/u2f_ex/tree/master)\n[![Hex.pm](https://img.shields.io/hexpm/v/u2f_ex.svg)](https://hex.pm/packages/u2f_ex)\n[HexDocs](https://hexdocs.pm/u2f_ex/api-reference.html)\n\nA Pure Elixir implementation of the U2F Protocol.\n\n## Installation\n\nIf [available in Hex](https://hex.pm/docs/publish), the package can be installed\nby adding `u2f_ex` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:u2f_ex, \"~\u003e 0.4.2\"}\n  ]\nend\n```\n\n### PKIStorage\n\nIn order to properly use this library, you're going to need to store metadata and public\nkeys for any user registering their U2F Token. However, u2f_ex will need to retrieve that \nmetadata, so you're get to write a glorious new module implementing our storage behaviour.\n\nCheck out some example docs here: [PKIStorage Example](https://hexdocs.pm/ecto/Ecto.Repo.html#c:list_key_handles_for_user/1)\n\n### Add A New SQL Table\n\nThis section assumes that you'll be using SQL as the primary storage mechanism for these keys,\nbut, if you plan on using something else, feel free to do so! Skip to the next section and, should \nyou have any questions, [feel free to ask!](https://github.com/GrappigPanda/u2f_ex/issues)\nFirst you'll want to create a model capable of representing the key metadata (you can steal the \nfollowing code):\n\n```elixir\ndefmodule Example.Users.U2FKey do\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias Example.Users.User\n\n  schema \"u2f_keys\" do\n    field(:public_key, :string, size: 128, null: false)\n    field(:key_handle, :string, size: 128, null: false)\n    field(:version, :string, size: 10, null: false, default: \"U2F_V2\")\n    field(:app_id, :string, null: false)\n    # NOTE: You'll need to update what table this references or change it to a normal field\n    belongs_to(:user, User)\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(user, attrs) do\n    user\n    |\u003e cast(attrs, [:public_key, :key_handle, :version, :app_id, :user_id])\n    |\u003e validate_required([:public_key, :key_handle, :version, :app_id, :user_id])\n    |\u003e validate_b64_string(:public_key)\n    |\u003e validate_b64_string(:key_handle)\n  end\n\n  @doc false\n  def validate_b64_string(changeset, field, opts \\\\ []) do\n    validate_change(changeset, field, fn _, value -\u003e\n      case Base.decode64(value, padding: false) do\n        {:ok, _result} -\u003e\n          []\n\n        _ -\u003e\n          [{field, opts[:message] || \"Invalid field #{field}. Expected b64 encoded string.\"}]\n      end\n    end)\n  end\nend\n```\n\nFinally, create and run the following migration:\n\n```elixir\ndefmodule Example.Repo.Migrations.AddU2fKey do\n  use Ecto.Migration\n\n  def change do\n    create table(:u2f_keys) do\n      add(:public_key, :string, size: 128)\n      add(:key_handle, :string, size: 128)\n      add(:version, :string, size: 10, default: \"U2F_V2\")\n      add(:app_id, :string)\n      # NOTE: You'll need to update what table this references or change it to a normal field\n      add(:user_id, references(:users))\n\n      timestamps()\n    end\n  end\nend\n```\n\n### Create a PKIStorage Module\n\nNext you'll need to provide the library a way of storing and fetching metadata about stored U2F keys,\nso you'll implement the [Storage Behaviour](https://hexdocs.pm/u2f_ex/U2FEx.PKIStorageBehaviour.html)\n\nAn example, that uses Ecto + SQL, will follow, but know that you can use whatever storage mechanism you \nwant so long as you adhere to the contract.\n\n```elixir\ndefmodule Example.PKIStorage do\n  @moduledoc false\n\n  import Ecto.Query\n\n  alias Example.Repo\n  alias U2FEx.PKIStorageBehaviour\n  alias Example.Users.U2FKey\n\n  @behaviour U2FEx.PKIStorageBehaviour\n\n  @impl PKIStorageBehaviour\n  def list_key_handles_for_user(user_id) do\n    q =\n      from(u in U2FKey,\n        where: u.user_id == ^user_id\n      )\n\n    x =\n      q\n      |\u003e Repo.all()\n      |\u003e Enum.map(fn %U2FKey{version: version, key_handle: key_handle, app_id: app_id} -\u003e\n        %{version: version, key_handle: key_handle, app_id: app_id}\n      end)\n\n    {:ok, x}\n  end\n\n  @impl PKIStorageBehaviour\n  def get_public_key_for_user(user_id, key_handle) do\n    q = from(u in U2FKey, where: u.user_id == ^user_id and u.key_handle == ^key_handle)\n\n    q\n    |\u003e Repo.one()\n    |\u003e case do\n      nil -\u003e {:error, :public_key_not_found}\n      %U2FKey{public_key: public_key} -\u003e {:ok, public_key}\n    end\n  end\nend\n```\n\n### Config Value\n\nNext you'll need to update your configuration to set the PKIStorage model:\n\n```elixir\nconfig :u2f_ex,\n    pki_storage: PKIStorage,\n    app_id: \"https://yoursite.com\"\n```\n###### NOTE: The \u003capp_id\u003e should be your site.\n\n### Create a Controller\n\nYou'll need a controller capable of handling these interactions:\n\n```elixir\ndefmodule ExampleWeb.U2FController do\n  use ExampleWeb, :controller\n\n  alias Example.Users\n  alias Example.Users.U2FKey\n  alias U2FEx.KeyMetadata\n\n  @doc \"\"\"\n  This is the first interaction in the u2f flow. We'll challenge the u2f token to\n  provide a public key and sign our challenge (+ other info) proving their ownership\n  of the corresponding private key.\n  \"\"\"\n  def start_registration(conn, _params) do\n    with {:ok, registration_data} \u003c- U2FEx.start_registration(get_user_id(conn)) do\n      output = %{\n        registerRequests: [\n          %{\n            appId: registration_data.appId,\n            padding: false,\n            version: \"U2F_V2\",\n            challenge: registration_data.challenge,\n            padding: false\n          }\n        ],\n        registeredKeys: []\n      }\n\n      conn\n      |\u003e json(output)\n    end\n  end\n\n  @doc \"\"\"\n  This is the second step of the registration where we'll store their key metadata for\n  use later in the authentication portion of the flow.\n  \"\"\"\n  def finish_registration(conn, device_response) do\n    user_id = get_user_id(conn)\n\n    with {:ok, %KeyMetadata{} = key_metadata} \u003c-\n           U2FEx.finish_registration(user_id, device_response),\n         :ok \u003c- store_key_data(user_id, key_metadata) do\n      conn\n      |\u003e json(%{\"success\" =\u003e true})\n    else\n      _error -\u003e\n        conn |\u003e put_status(:bad_request) |\u003e json(%{\"success\" =\u003e false})\n    end\n  end\n\n  @doc \"\"\"\n  Should the user be logging in, and they have a u2f key registered in our system, we\n  should challenge that user to prove their identity and ownership of the u2f device.\n  \"\"\"\n  def start_authentication(conn, _params) do\n    with {:ok, %{} = sign_request} \u003c- U2FEx.start_authentication(get_user_id(conn)) do\n      conn\n      |\u003e json(sign_request)\n    end\n  end\n\n  @doc \"\"\"\n  After the user has attempted to verify their identity, U2FEx will verify they actually who are\n  they say they are. Once this step has exited successfully, then we can be reasonably assured the\n  user is who they claim to be.\n  \"\"\"\n  def finish_authentication(conn, device_response) do\n    with :ok \u003c- U2FEx.finish_authentication(get_user_id(conn), device_response |\u003e Jason.encode!()) do\n      conn\n      |\u003e json(%{\"success\" =\u003e true})\n    else\n      _ -\u003e json(conn, %{\"success\" =\u003e false})\n    end\n  end\n\n  @doc \"\"\"\n  Fill in with however you want to persist keys. See U2FEx.KeyMetadata struct for more info\n  \"\"\"\n  @spec store_key_data(user_id :: any(), KeyMetadata.t()) :: :ok | {:error, any()}\n  def store_key_data(user_id, key_metadata) do\n    with {:ok, %U2FKey{}} \u003c- Users.create_u2f_key(user_id, key_metadata) do\n      :ok\n    end\n  end\n\n  @spec get_user_id(Plug.Conn.t()) :: String.t()\n  defp get_user_id(_conn) do\n    \"1\"\n  end\nend\n```\n\nMoreover, you're going to need to add routes (feel free to change, but you need these four routes specifically).\n\n```elixir\n    post(\"/u2f/start_registration\", U2FController, :start_registration)\n    post(\"/u2f/finish_registration\", U2FController, :finish_registration)\n    post(\"/u2f/start_authentication\", U2FController, :start_authentication)\n    post(\"/u2f/finish_authentication\", U2FController, :finish_authentication)\n```\n\n### Finally, finish up with some javascript\n\nVendor google's u2f-api-polyfill.js (Can be found [here](https://raw.githubusercontent.com/mastahyeti/u2f-api/master/u2f-api-polyfill.js) or [here](https://github.com/GrappigPanda/u2f_ex/blob/7223f588d03a6c472b1988de08428377f0a3dec9/example/assets/vendor/u2f-api-polyfill.js)).\n\nFinally, you'll need to handle events for talking to the device. This assumes jquery, but it can be\neasily swapped out and work in vanilla Javascript, React, Vue, \u0026c.\n\n```javascript\nimport $ from \"jquery\";\n\n$(document).ready(() =\u003e {\n  const appId = \"https://localhost\";\n  const u2f = window.u2f;\n  const post = (url, csrf, data) =\u003e {\n    return $.ajax({\n      url: url,\n      type: \"POST\",\n      dataType: \"json\",\n      contentType: \"application/json\",\n      data: JSON.stringify(data),\n      beforeSend: xhr =\u003e {\n        xhr.setRequestHeader(\"X-CSRF-TOKEN\", csrf);\n      }\n    });\n  };\n\n  $(\"#register\").click(() =\u003e {\n    const csrf = $(\"meta[name='csrf-token']\").attr(\"content\");\n    post(\"/u2f/start_registration\", csrf).then(\n      ({ appId, registerRequests, registeredKeys }) =\u003e {\n        u2f.register(appId, registerRequests, registeredKeys, response =\u003e {\n          post(\"/u2f/finish_registration\", csrf, response)\n            // NOTE: Handle finishing registration here\n                .then(x =\u003e console.log(\"Finished Registration\"));\n        });\n      },\n      error =\u003e {\n        console.error(error);\n      }\n    );\n  });\n\n  $(\"#sign\").click(() =\u003e {\n    const csrf = $(\"meta[name='csrf-token']\").attr(\"content\");\n    post(\"/u2f/start_authentication\", csrf).then(\n      ({ challenge, registeredKeys }) =\u003e {\n        u2f\n          .sign(appId, challenge, registeredKeys, response1 =\u003e {\n            post(\"/u2f/finish_authentication\", csrf, response1).then(\n              // NOTE: Handle finishing authentication here\n              x =\u003e console.log(\"Finished Authentication\")\n            );\n          });\n      },\n      error =\u003e {\n        console.error(error);\n      }\n    );\n  });\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fianleeclark%2Fu2f_ex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fianleeclark%2Fu2f_ex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fianleeclark%2Fu2f_ex/lists"}