All Projects → Ianleeclark → u2f_ex

Ianleeclark / u2f_ex

Licence: BSD-3-Clause license
A server-side U2F (Universal Second Factor) library in Elixir

Programming Languages

elixir
2628 projects
javascript
184084 projects - #8 most used programming language
HTML
75241 projects

Projects that are alternatives of or similar to u2f ex

u2f-php
FIDO/FIDO2 Universal 2 Factors (U2F) support for PHP
Stars: ✭ 25 (+0%)
Mutual labels:  u2f, u2f-server
Opensk
OpenSK is an open-source implementation for security keys written in Rust that supports both FIDO U2F and FIDO2 standards.
Stars: ✭ 2,114 (+8356%)
Mutual labels:  u2f
yubitell
Silently extract a YubiKey serial number
Stars: ✭ 15 (-40%)
Mutual labels:  u2f
awesome-yubikey
Curated list of awesome Yubikey resources, open source projects, tools and tutorials.
Stars: ✭ 22 (-12%)
Mutual labels:  u2f
CCU2F
An universal usable FIDO U2F authenticator applet for Java Cards
Stars: ✭ 32 (+28%)
Mutual labels:  u2f
wp-webauthn
🔒 WP-WebAuthn allows you to safely login to your WordPress site without password.
Stars: ✭ 85 (+240%)
Mutual labels:  u2f
libu2f-emu
Universal 2nd Factor (U2F) Emulation C Library
Stars: ✭ 35 (+40%)
Mutual labels:  u2f
u2f-luks
No description or website provided.
Stars: ✭ 29 (+16%)
Mutual labels:  u2f
Solo
Solo 1: open security key supporting FIDO2 & U2F over USB + NFC
Stars: ✭ 1,986 (+7844%)
Mutual labels:  u2f
u2fdemo
U2F Demo and Debugger
Stars: ✭ 23 (-8%)
Mutual labels:  u2f
traefik-proxy
One-step (secure) configuration for Traefik edge router.
Stars: ✭ 20 (-20%)
Mutual labels:  u2f
U2F Core
U2F library for .NET Core
Stars: ✭ 37 (+48%)
Mutual labels:  u2f
keymaster
Short term certificate based identity system (ssh/x509 ca + openidc)
Stars: ✭ 59 (+136%)
Mutual labels:  u2f
U2f Zero
U2F USB token optimized for physical security, affordability, and style
Stars: ✭ 2,275 (+9000%)
Mutual labels:  u2f
kagi
WebAuthn security keys and TOTP multi-factor authentication for Django
Stars: ✭ 17 (-32%)
Mutual labels:  u2f
authenticator-rs
Rust library to interact with Security Keys, used by Firefox
Stars: ✭ 209 (+736%)
Mutual labels:  u2f
lsso
Nginx SSO middleware for protecting your internets.
Stars: ✭ 42 (+68%)
Mutual labels:  u2f
rx-otp
HMAC-based (HOTP) and Time-based (TOTP) One-Time Password manager. Works with Google Authenticator for Two-Factor Authentication.
Stars: ✭ 79 (+216%)
Mutual labels:  u2f
clarion
WebAuthn (U2F) helper for CLI operations (e.g. SSH Log in)
Stars: ✭ 78 (+212%)
Mutual labels:  u2f
Authelia
The Single Sign-On Multi-Factor portal for web apps
Stars: ✭ 11,094 (+44276%)
Mutual labels:  u2f

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:

def deps do
  [
    {:u2f_ex, "~> 0.4.2"}
  ]
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):

defmodule Example.Users.U2FKey do
  use Ecto.Schema
  import Ecto.Changeset

  alias Example.Users.User

  schema "u2f_keys" do
    field(:public_key, :string, size: 128, null: false)
    field(:key_handle, :string, size: 128, null: false)
    field(:version, :string, size: 10, null: false, default: "U2F_V2")
    field(:app_id, :string, null: false)
    # NOTE: You'll need to update what table this references or change it to a normal field
    belongs_to(:user, User)

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:public_key, :key_handle, :version, :app_id, :user_id])
    |> validate_required([:public_key, :key_handle, :version, :app_id, :user_id])
    |> validate_b64_string(:public_key)
    |> validate_b64_string(:key_handle)
  end

  @doc false
  def validate_b64_string(changeset, field, opts \\ []) do
    validate_change(changeset, field, fn _, value ->
      case Base.decode64(value, padding: false) do
        {:ok, _result} ->
          []

        _ ->
          [{field, opts[:message] || "Invalid field #{field}. Expected b64 encoded string."}]
      end
    end)
  end
end

Finally, create and run the following migration:

defmodule Example.Repo.Migrations.AddU2fKey do
  use Ecto.Migration

  def change do
    create table(:u2f_keys) do
      add(:public_key, :string, size: 128)
      add(:key_handle, :string, size: 128)
      add(:version, :string, size: 10, default: "U2F_V2")
      add(:app_id, :string)
      # NOTE: You'll need to update what table this references or change it to a normal field
      add(:user_id, references(:users))

      timestamps()
    end
  end
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.

defmodule Example.PKIStorage do
  @moduledoc false

  import Ecto.Query

  alias Example.Repo
  alias U2FEx.PKIStorageBehaviour
  alias Example.Users.U2FKey

  @behaviour U2FEx.PKIStorageBehaviour

  @impl PKIStorageBehaviour
  def list_key_handles_for_user(user_id) do
    q =
      from(u in U2FKey,
        where: u.user_id == ^user_id
      )

    x =
      q
      |> Repo.all()
      |> Enum.map(fn %U2FKey{version: version, key_handle: key_handle, app_id: app_id} ->
        %{version: version, key_handle: key_handle, app_id: app_id}
      end)

    {:ok, x}
  end

  @impl PKIStorageBehaviour
  def get_public_key_for_user(user_id, key_handle) do
    q = from(u in U2FKey, where: u.user_id == ^user_id and u.key_handle == ^key_handle)

    q
    |> Repo.one()
    |> case do
      nil -> {:error, :public_key_not_found}
      %U2FKey{public_key: public_key} -> {:ok, public_key}
    end
  end
end

Config Value

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

config :u2f_ex,
    pki_storage: PKIStorage,
    app_id: "https://yoursite.com"
NOTE: The <app_id> should be your site.

Create a Controller

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

defmodule ExampleWeb.U2FController do
  use ExampleWeb, :controller

  alias Example.Users
  alias Example.Users.U2FKey
  alias U2FEx.KeyMetadata

  @doc """
  This is the first interaction in the u2f flow. We'll challenge the u2f token to
  provide a public key and sign our challenge (+ other info) proving their ownership
  of the corresponding private key.
  """
  def start_registration(conn, _params) do
    with {:ok, registration_data} <- U2FEx.start_registration(get_user_id(conn)) do
      output = %{
        registerRequests: [
          %{
            appId: registration_data.appId,
            padding: false,
            version: "U2F_V2",
            challenge: registration_data.challenge,
            padding: false
          }
        ],
        registeredKeys: []
      }

      conn
      |> json(output)
    end
  end

  @doc """
  This is the second step of the registration where we'll store their key metadata for
  use later in the authentication portion of the flow.
  """
  def finish_registration(conn, device_response) do
    user_id = get_user_id(conn)

    with {:ok, %KeyMetadata{} = key_metadata} <-
           U2FEx.finish_registration(user_id, device_response),
         :ok <- store_key_data(user_id, key_metadata) do
      conn
      |> json(%{"success" => true})
    else
      _error ->
        conn |> put_status(:bad_request) |> json(%{"success" => false})
    end
  end

  @doc """
  Should the user be logging in, and they have a u2f key registered in our system, we
  should challenge that user to prove their identity and ownership of the u2f device.
  """
  def start_authentication(conn, _params) do
    with {:ok, %{} = sign_request} <- U2FEx.start_authentication(get_user_id(conn)) do
      conn
      |> json(sign_request)
    end
  end

  @doc """
  After the user has attempted to verify their identity, U2FEx will verify they actually who are
  they say they are. Once this step has exited successfully, then we can be reasonably assured the
  user is who they claim to be.
  """
  def finish_authentication(conn, device_response) do
    with :ok <- U2FEx.finish_authentication(get_user_id(conn), device_response |> Jason.encode!()) do
      conn
      |> json(%{"success" => true})
    else
      _ -> json(conn, %{"success" => false})
    end
  end

  @doc """
  Fill in with however you want to persist keys. See U2FEx.KeyMetadata struct for more info
  """
  @spec store_key_data(user_id :: any(), KeyMetadata.t()) :: :ok | {:error, any()}
  def store_key_data(user_id, key_metadata) do
    with {:ok, %U2FKey{}} <- Users.create_u2f_key(user_id, key_metadata) do
      :ok
    end
  end

  @spec get_user_id(Plug.Conn.t()) :: String.t()
  defp get_user_id(_conn) do
    "1"
  end
end

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

    post("/u2f/start_registration", U2FController, :start_registration)
    post("/u2f/finish_registration", U2FController, :finish_registration)
    post("/u2f/start_authentication", U2FController, :start_authentication)
    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.

import $ from "jquery";

$(document).ready(() => {
  const appId = "https://localhost";
  const u2f = window.u2f;
  const post = (url, csrf, data) => {
    return $.ajax({
      url: url,
      type: "POST",
      dataType: "json",
      contentType: "application/json",
      data: JSON.stringify(data),
      beforeSend: xhr => {
        xhr.setRequestHeader("X-CSRF-TOKEN", csrf);
      }
    });
  };

  $("#register").click(() => {
    const csrf = $("meta[name='csrf-token']").attr("content");
    post("/u2f/start_registration", csrf).then(
      ({ appId, registerRequests, registeredKeys }) => {
        u2f.register(appId, registerRequests, registeredKeys, response => {
          post("/u2f/finish_registration", csrf, response)
            // NOTE: Handle finishing registration here
                .then(x => console.log("Finished Registration"));
        });
      },
      error => {
        console.error(error);
      }
    );
  });

  $("#sign").click(() => {
    const csrf = $("meta[name='csrf-token']").attr("content");
    post("/u2f/start_authentication", csrf).then(
      ({ challenge, registeredKeys }) => {
        u2f
          .sign(appId, challenge, registeredKeys, response1 => {
            post("/u2f/finish_authentication", csrf, response1).then(
              // NOTE: Handle finishing authentication here
              x => console.log("Finished Authentication")
            );
          });
      },
      error => {
        console.error(error);
      }
    );
  });
});
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].