Skip to content

Commit

Permalink
76 Separate Authentication & Registration Views (#79)
Browse files Browse the repository at this point in the history
* fix: use actual app name instead of dirname

* add registration route and navbar link

* bump version to `0.8.0`

* add passkey components

* split registration and authentication views

* fix automatic prompt for auth with existing passkeys

* update auth live test

* rm unnecessary assign reset
  • Loading branch information
type1fool authored Jul 17, 2024
1 parent 0503e5d commit ff95d04
Show file tree
Hide file tree
Showing 17 changed files with 387 additions and 251 deletions.
8 changes: 4 additions & 4 deletions lib/mix/tasks/wac.install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ defmodule Mix.Tasks.Wac.Install do
case OptionParser.parse(args, strict: @switches) do
{flags, _args, []} ->
opts = Keyword.merge(@default_opts, flags)
dirname = File.cwd!() |> Path.basename()
dirname_camelized = Macro.camelize(dirname)
web_dirname = dirname <> "_web"
app_name = Mix.Project.config() |> Keyword.fetch!(:app) |> to_string()
dirname_camelized = Macro.camelize(app_name)
web_dirname = app_name <> "_web"
web_dirname_camelized = Macro.camelize(web_dirname)

assigns = [
app_snake_case: dirname,
app_snake_case: app_name,
app_pascal_case: Module.concat([dirname_camelized]),
web_snake_case: web_dirname,
web_pascal_case: Module.concat([web_dirname_camelized])
Expand Down
5 changes: 4 additions & 1 deletion lib/wac_gen/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ defmodule Wac.Gen.Components do
@template_files %{
navigation_components: "navigation_components.ex",
navbar: "navigation/navbar.html.heex",
nav_link: "navigation/nav_link.html.heex"
nav_link: "navigation/nav_link.html.heex",
passkey_components: "passkey_components.ex",
guidance: "passkeys/guidance.html.heex",
token_form: "passkeys/token_form.html.heex"
}

@templates Map.keys(@template_files)
Expand Down
4 changes: 3 additions & 1 deletion lib/wac_gen/live_views.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ defmodule Wac.Gen.LiveViews do

@template_files %{
authentication: "authentication_live.ex",
authentication_html: "authentication_live.html.heex"
authentication_html: "authentication_live.html.heex",
registration: "registration_live.ex",
registration_html: "registration_live.html.heex"
}

@templates Map.keys(@template_files)
Expand Down
1 change: 1 addition & 0 deletions lib/wac_gen/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ defmodule Wac.Gen.Router do
scope "/", #{inspect(web_pascal_case)} do
pipe_through :browser
live "/sign-up", RegistrationLive
live "/sign-in", AuthenticationLive
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/webauthn_components/authentication_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ defmodule WebauthnComponents.AuthenticationComponent do
<span :if={@show_icon?} class="w-4 aspect-square opacity-70"><.icon_key /></span>
<span><%= @display_text %></span>
</.button>
<input type="hidden" autocomplete="webauthn" />
</span>
"""
end
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule WebauthnComponents.MixProject do
# Don't forget to change the version in `package.json`
@name "WebauthnComponents"
@source_url "https://github.com/liveshowy/webauthn_components"
@version "0.7.3"
@version "0.8.0"

def project do
[
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webauthn_components",
"version": "0.7.3",
"version": "0.8.0",
"main": "./priv/static/main.js",
"repository": {},
"files": [
Expand Down
1 change: 1 addition & 0 deletions templates/components/navigation/navbar.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<span class="flex-grow" />

<%!-- Unauthenticated Routes --%>
<.nav_link :if={!@current_user} navigate={~p"/sign-up"}>Sign Up</.nav_link>
<.nav_link :if={!@current_user} navigate={~p"/sign-in"}>Sign In</.nav_link>

<%!-- Authenticated Routes --%>
Expand Down
17 changes: 17 additions & 0 deletions templates/components/passkey_components.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule <%= inspect @web_pascal_case %>.PasskeyComponents do
@moduledoc """
Components for navigating the application.
"""
use Phoenix.Component
use <%= inspect @web_pascal_case %>, :verified_routes
import <%= inspect @web_pascal_case %>.CoreComponents
alias Phoenix.LiveView.JS

embed_templates "/passkeys/*"
def guidance(assigns)

attr :form, Phoenix.HTML.Form, required: true, doc: "Form used to create a session upon successful registration or authentication."

def token_form(assigns)
end
33 changes: 33 additions & 0 deletions templates/components/passkeys/guidance.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<details class="grid gap-4 bg-gray-50 ring-1 ring-gray-200 shadow-lg rounded-lg group">
<summary class={[
"px-4 py-2 cursor-pointer transition select-none",
"bg-gray-50 hover:bg-gray-100 font-semibold",
"rounted-t-lg rounded-b-lg group-open:rounded-b-none",
"group-open:border-b-2 group-open:border-gray-300"
]}>
Where's the password?
</summary>

<div class="py-4 px-8 text-justify grid gap-2">
<p>
This application uses a new technology that replaces passwords and temporary codes.
</p>

<p>
<a href="https://fidoalliance.org/passkeys/" class="font-bold" target="_blank">
Passkeys
</a>
are a more secure way to sign into applications. They use advanced technology embraced by security experts, while improving ease-of-use.
</p>

<p>
Read more about Passkeys at the <a
href="https://fidoalliance.org/passkeys/#faq"
class="font-bold"
target="_blank"
>
FIDO Alliance
</a>.
</p>
</div>
</details>
20 changes: 20 additions & 0 deletions templates/components/passkeys/token_form.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<%!--
- The form is only rendered when a token has been created by the registration or authentication component.
- The form is present in the markup, but hidden from view.
- When the form is mounted, JS is used to click the submit button.
- This is a bit hacky, but it works.
--%>

<.simple_form
:let={f}
:if={@form}
for={@form}
method="post"
id="token-form"
action={~p"/session"}
phx-mounted={JS.dispatch("click", to: "#token-form button[type='submit']")}
class="hidden"
>
<.input type="text" field={f[:value]} label="Value" />
<.button type="submit">Go</.button>
</.simple_form>
72 changes: 1 addition & 71 deletions templates/live_views/authentication_live.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule <%= inspect @web_pascal_case %>.AuthenticationLive do
@moduledoc """
LiveView for registering new users and authenticating existing users.
LiveView for authenticating **existing** users.
See `WebauthnComponents` for details on Passkey authentication.
"""
Expand All @@ -12,9 +12,7 @@ defmodule <%= inspect @web_pascal_case %>.AuthenticationLive do
alias <%= inspect @app_pascal_case %>.Identity.UserToken

alias WebauthnComponents.SupportComponent
alias WebauthnComponents.RegistrationComponent
alias WebauthnComponents.AuthenticationComponent
alias WebauthnComponents.WebauthnUser

def mount(_params, _user_id, %{assigns: %{current_user: %User{}}} = socket) do
{
Expand All @@ -25,44 +23,15 @@ defmodule <%= inspect @web_pascal_case %>.AuthenticationLive do
end

def mount(_params, _session, socket) do
webauthn_user = %WebauthnUser{id: generate_encoded_id(), name: nil, display_name: nil}

if connected?(socket) do
send_update(RegistrationComponent,
id: "registration-component",
webauthn_user: webauthn_user
)
end

{
:ok,
socket
|> assign(:page_title, "Sign In")
|> assign(:form, build_form())
|> assign(:show_registration?, true)
|> assign(:show_authentication?, true)
|> assign(:webauthn_user, webauthn_user)
|> assign(:token_form, nil)
}
end

def handle_event("update-form", %{"email" => email} = params, socket) do
%{webauthn_user: webauthn_user} = socket.assigns
webauthn_user =
webauthn_user
|> Map.put(:name, email)
|> Map.put(:display_name, email)
send_update(RegistrationComponent, id: "registration-component", webauthn_user: webauthn_user)

{
:noreply,
socket
|> assign(:form, build_form(params))
}
end

def handle_event(event, params, socket) do
Logger.warning(unhandled_event: {__MODULE__, event, params})
{:noreply, socket}
Expand All @@ -76,34 +45,7 @@ defmodule <%= inspect @web_pascal_case %>.AuthenticationLive do
:noreply,
socket
|> put_flash(:error, "Passkeys are not supported in this browser.")
|> assign(:form, nil)
}
end
end
def handle_info({:registration_successful, params}, socket) do
%{form: form} = socket.assigns
user_attrs = %{email: form[:email].value, keys: [params[:key]]}
with {:ok, %User{id: user_id}} <- Identity.create(user_attrs),
{:ok, %UserToken{value: token_value}} <- Identity.create_token(%{user_id: user_id}) do
encoded_token = Base.encode64(token_value, padding: false)
token_attrs = %{"value" => encoded_token}
{
:noreply,
socket
|> assign(:token_form, to_form(token_attrs, as: "token"))
}
else
{:error, changeset} ->
Logger.error(registration_error: {__MODULE__, changeset.changes, changeset.errors})
{
:noreply,
socket
|> assign(:form, to_form(changeset))
}
end
end

Expand Down Expand Up @@ -176,16 +118,4 @@ defmodule <%= inspect @web_pascal_case %>.AuthenticationLive do
Logger.warning(unhandled_message: {__MODULE__, message})
{:noreply, socket}
end

defp build_form(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Map.put(:action, :insert)
|> to_form()
end

defp generate_encoded_id do
:crypto.strong_rand_bytes(64)
|> Base.encode64(padding: false)
end
end
Loading

0 comments on commit ff95d04

Please sign in to comment.