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

76 Separate Authentication & Registration Views #79

Merged
merged 8 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading