Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add u2f #82

Merged
merged 12 commits into from
Jul 22, 2023
21 changes: 21 additions & 0 deletions apps/ex_fleet_yards/lib/ex_fleet_yards/repo/account/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ defmodule ExFleetYards.Repo.Account.User do
import Ecto.Changeset
import ExFleetYards.Repo.Changeset

alias ExFleetYards.Repo

@primary_key {:id, Ecto.UUID, []}

typed_schema "users" do
Expand Down Expand Up @@ -49,10 +51,29 @@ defmodule ExFleetYards.Repo.Account.User do
has_many :vehicles, ExFleetYards.Repo.Account.Vehicle
has_many :sso_connections, ExFleetYards.Repo.Account.User.SSOConnection
has_one :totp, ExFleetYards.Repo.Account.User.Totp
has_many :u2f_token, ExFleetYards.Repo.Account.User.U2fToken

timestamps(inserted_at: :created_at, type: :utc_datetime)
end

@doc """
Returns if the user has u2f and totp enabled as tupl
"""
@spec second_factors(__MODULE__.t() | Ecto.UUID.t()) ::
{:ok, {boolean(), boolean()}}
| {:error, any()}
| {:error, Ecto.Multi.name(), any(), any()}
def second_factors(%__MODULE__{id: user_id}), do: second_factors(user_id)

def second_factors(user) when is_binary(user) do
Repo.transaction(fn ->
u2f = Repo.exists?(__MODULE__.U2fToken.user_query(user))
totp = Repo.exists?(__MODULE__.Totp.user_query(user))

{u2f, totp}
end)
end

def info_changeset(user, attrs) do
user
|> cast(attrs, [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule ExFleetYards.Repo.Account.User.U2fToken do
@moduledoc """
U2F authentication data
"""

use TypedEctoSchema

alias ExFleetYards.Repo
alias ExFleetYards.Repo.Account.User

@primary_key {:id, Ecto.UUID, []}

typed_schema "user_u2f_tokens" do
field :name, :string
field :credential_id, :binary
field :cose_key, :binary

belongs_to :user, User, type: Ecto.UUID

timestamps(inserted_at: :created_at, updated_at: false)
end

def create(user, credential_id, cose_key, name \\ nil)

def create(%User{id: user_id}, credential_id, cose_key, name),
do: create(user_id, credential_id, cose_key, name)

def create(user_id, credential_id, cose_key, name) when is_binary(user_id) do
%__MODULE__{}
|> create_changeset(%{
user_id: user_id,
credential_id: credential_id,
cose_key: :erlang.term_to_binary(cose_key),
name: nil
})
|> Repo.insert()
end

import Ecto.Query

def user_query(user_id) when is_binary(user_id) do
from u in __MODULE__, where: u.user_id == ^user_id
end

import Ecto.Changeset

def create_changeset(token \\ %__MODULE__{}, attrs) do
token
|> cast(attrs, [:user_id, :credential_id, :cose_key, :name])
|> validate_required([:user_id, :credential_id, :cose_key])
|> unique_constraint([:credential_id])
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule ExFleetYards.Repo.Migrations.UserU2fToken do
use Ecto.Migration

def change do
create table(:user_u2f_tokens, primary_key: false) do
add :id, :uuid, primary_key: true, null: false, default: fragment("gen_random_uuid()")
add :name, :string
add :credential_id, :binary, null: false
add :cose_key, :binary, null: false

add :user_id,
references(:users, type: :uuid, primary_key: true, on_delete: :delete_all, null: false)

timestamps(inserted_at: :created_at, updated_at: false)
end

create index(:user_u2f_tokens, [:user_id])
create unique_index(:user_u2f_tokens, [:credential_id])
end
end
85 changes: 85 additions & 0 deletions apps/ex_fleet_yards_auth/assets/js/u2f.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict';

function isWebAuthnSupported() {
return window.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === "function"
}

function getLabel() {
return document.getElementById('u2f_token_name').value
}

function getCsrfToken() {
return document.head.querySelector('meta[name="csrf-token"]').content
}


function webAuthnRegister() {
if (!isWebAuthnSupported()) {
alert("Sorry, Webauthn is not supported by your browser")
return
}

const label = getLabel();
if (label == "") {
alert("No label given") // TODO: replace with inline error message
return
}

fetch("/u2f/challenge", {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
"label": label,
}),
headers: {
"Content-Type": "application/json",
"X-csrf-token": getCsrfToken()
}
})
.then(res => res.json())
.then(response => {
const challenge = response.cc;
challenge.publicKey.challenge = fromBase64(challenge.publicKey.challenge);
console.log(toBase64(challenge.publicKey.challenge))
challenge.publicKey.user.id = fromBase64(challenge.publicKey.user.id);
return navigator.credentials.create(challenge).then(newCredential => {
const cc = {};
cc.id = newCredential.id;
cc.rawId = toBase64(newCredential.rawId);
cc.response = {};
cc.response.attestationObject = toBase64(newCredential.response.attestationObject);
cc.response.clientDataJSON = toBase64(newCredential.response.clientDataJSON);
cc.type = newCredential.type;
cc.name = getLabel();
fetch("/u2f/challenge/register/" + response.id, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(cc),
headers: {
"Content-Type": "application/json",
"X-csrf-token": getCsrfToken()
},
}).then(res => {
if (res.status != 201) {
alert("There is an internal error. Try again later.") // replace with inline error message
throw new Error("Oopps");
}
document.location.reload(); // TODO: replace with live view?
})
})
})
}

function toBase64(data) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(data)));
}

function fromBase64(data) {
return toArray(atob(data))
}

function toArray(str) {
return Uint8Array.from(str, c => c.charCodeAt(0));
}

document.getElementById("u2fRegisterButton").addEventListener('click', webAuthnRegister)
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
defmodule ExFleetYardsAuth.U2F.RegisterController do
@moduledoc """
U2F controller to register new keys
"""
use ExFleetYardsAuth, :controller
require Logger

alias ExFleetYards.Repo.Account.User

plug :put_view, html: ExFleetYardsAuth.U2F.RegisterHTML

def index(conn, %{}) do
user = conn.assigns[:current_user]
{:ok, {_, totp}} = User.second_factors(user)

conn
|> render("index.html",
u2f_tokens: [],
totp_tokens: totp
)
end

def challenge(conn, %{}) do
user = conn.assigns[:current_user]

challenge = Wax.new_registration_challenge(register_opts())

conn
|> put_session(:challenge, challenge)
|> json(%{
id: "foo",
cc: %{
publicKey: %{
challenge: Base.encode64(challenge.bytes),
rp: %{
id: challenge.rp_id,
name: "Fleetyards SSO"
},
user: %{
id: Base.encode64(user.id),
name: user.email,
displayName: user.username
},
attestation: challenge.attestation,
authenticatorSelection: %{
requireResidentKey: false,
userVerification: challenge.user_verification
},
timeout: challenge.timeout,
pubKeyCredParams: [
%{type: "public-key", alg: -7},
%{type: "public-key", alg: -257}
]
}
}
})
end

def register(conn, %{"name" => name, "response" => response, "rawId" => rawId} = data) do
Fixed Show fixed Hide fixed
user = conn.assigns[:current_user]
challenge = get_session(conn, :challenge)

attestation_object = Base.decode64!(response["attestationObject"])
clientData = Base.decode64!(response["clientDataJSON"])
Fixed Show fixed Hide fixed

with {:ok, {authenticator_data, result}} <-
Wax.register(attestation_object, clientData, challenge),
{:ok, token} <-
User.U2fToken.create(
user,
rawId,
authenticator_data.attested_credential_data.credential_public_key,
name
) do
Logger.debug("Wax: registered new key", user_id: user.id)

conn
|> put_status(:created)
|> json(%{"name" => name})
else
{:error, e} ->
Logger.debug("Wax: failed: #{inspect(e)}")

conn
|> put_status(:bad_request)
|> json(%{"name" => name})
end
end

defp register_opts do
[
origin: ExFleetYards.Config.get(:boruta, [Boruta.Oauth, :issuer]),
attestation: "direct"
]
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule ExFleetYardsAuth.U2F.RegisterHTML do
use ExFleetYardsAuth, :html

embed_templates "register_html/*"
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script defer="" phx-track-static="" type="text/javascript" src={~p"/assets/u2f.js"} />
<div class="min-h-screen bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<img class="mx-auto h-12 w-auto" src={~p"/images/logo-dark.png"} alt="Logo" />

<h2 class="mt-6 text-center text-3xl font-extrabold text-white">
U2F Token Register and Management
</h2>
</div>

<%= if !@totp_tokens do %>
<a href={~p"/totp"}>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md py-2 px-4 bg-yellow-500 rounded-md">
<p class="text-md font-medium text-white">
No TOTP tokens are configured. You will not be able to recover your account if you lose your U2F tokens.
</p>
</div>
</a>
<% end %>

<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<.form :let={f} for={%{}} as={:u2f} action={~p"/register_u2f_token"} method="post">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<p class="text-md font-medium text-white">Registered U2F Tokens:</p>
<ul class="text-white">
<%= Enum.each(@u2f_tokens, fn token -> %>
<li><%= token.name %></li>
<% end ) %>
</ul>
</div>
<%= if length(@u2f_tokens) == 0 do %>
<% end %>
</div>

<div class="rounded-md shadow-sm -space-y-px mt-4">
<div>
<label for="otp_code" class="sr-only">U2F Token Registration</label>
<input
id="u2f_token_name"
name="u2f_token"
type="text"
required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-500 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Enter U2F Token Name"
/>
</div>
</div>

<div class="mt-6">
<button
type="button"
id="u2fRegisterButton"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
<.icon name="hero-lock-closed-solid" />
</span>
Register U2F Token
</button>
<button type="submit" />
</div>
</.form>
</div>
</div>
</div>
Loading
Loading