Skip to content

Commit

Permalink
Update README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
Ianleeclark authored Dec 2, 2018
1 parent 7bd1095 commit 796ada3
Showing 1 changed file with 289 additions and 4 deletions.
293 changes: 289 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Hex.pm](https://img.shields.io/hexpm/v/u2f_ex.svg)](https://hex.pm/packages/u2f_ex)
[HexDocs](https://hexdocs.pm/u2f_ex/api-reference.html)

**TODO: Add description**
A Pure Elixir implementation of the U2F Protocol.

## Installation

Expand All @@ -26,6 +26,128 @@ metadata, so you're get to write a glorious new module implementing our storage

Check out some example docs here: [PKIStorage Example](https://hexdocs.pm/ecto/Ecto.Repo.html#c:list_key_handles_for_user/1)

### 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!](https://github.com/GrappigPanda/u2f_ex/issues)
First you'll want to create a model capable of representing the key metadata (you can steal the
following code):

```elixir
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:

```elixir
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](https://hexdocs.pm/u2f_ex/U2FEx.PKIStorageBehaviour.html)

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.

```elixir
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:
Expand All @@ -37,7 +159,170 @@ config :u2f_ex,
```
###### NOTE: The <app_id> should be your site.

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/u2f_ex](https://hexdocs.pm/u2f_ex).
### Create a Controller

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

```elixir
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).

```elixir
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](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)).

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.

```javascript
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);
}
);
});
});
```

0 comments on commit 796ada3

Please sign in to comment.