Skip to content

Commit

Permalink
Improve passkey UX
Browse files Browse the repository at this point in the history
  • Loading branch information
sfnelson committed Nov 21, 2024
1 parent a7d42c9 commit c2d34a7
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import {
} from "@github/webauthn-json/browser-ponyfill";

export default class WebauthnRegistrationController extends Controller {
static values = { options: Object };
static targets = ["response"];
static values = {
options: Object,
response: String,
};
static targets = ["intro", "nickname", "response"];

submit(e) {
if (this.responseTarget.value === "") {
Expand All @@ -16,12 +19,17 @@ export default class WebauthnRegistrationController extends Controller {
}
}

createCredential() {
create(this.options).then((response) => {
this.responseTarget.value = JSON.stringify(response);
async createCredential() {
const response = await create(this.options);

this.element.requestSubmit();
});
this.responseValue = JSON.stringify(response);
this.responseTarget.value = JSON.stringify(response);
}

responseValueChanged(response) {
const responsePresent = response !== "";
this.introTarget.toggleAttribute("hidden", responsePresent);
this.nicknameTarget.toggleAttribute("hidden", !responsePresent);
}

get options() {
Expand Down
8 changes: 8 additions & 0 deletions app/assets/stylesheets/koi/base/_flow.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
FLOW COMPOSITION
Like the Every Layout stack: https://every-layout.dev/layouts/stack/
Info about this implementation: https://piccalil.li/quick-tip/flow-utility/
*/
.flow > * + * {
margin-top: var(--flow-space, 1em);
}
2 changes: 2 additions & 0 deletions app/assets/stylesheets/koi/base/_index.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
@use "button";
@use "icon";
@use "input";
@use "flow";
@use "link";
@use "list";
@use "repel";
@use "tables";
@use "typography";

Expand Down
23 changes: 23 additions & 0 deletions app/assets/stylesheets/koi/base/_repel.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
REPEL
A little layout that pushes items away from each other where
there is space in the viewport and stacks on small viewports
CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-s-m)): This defines the space
between each item.
--repel-vertical-alignment How items should align vertically.
Can be any acceptable flexbox alignment value.
*/
.repel {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: var(--repel-vertical-alignment, center);
gap: var(--gutter, var(--space-s-m));
}

.repel[data-nowrap] {
flex-wrap: nowrap;
}
3 changes: 2 additions & 1 deletion app/controllers/concerns/koi/controller/has_webauthn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ def webauthn_authenticate!
Admin::Credential.find_by!(external_id: credential.id)
end

stored_credential.update!(sign_count: webauthn_credential.sign_count)
stored_credential.update(sign_count: webauthn_credential.sign_count)
stored_credential.touch

stored_credential.admin
end
Expand Down
3 changes: 3 additions & 0 deletions app/views/admin/admin_users/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<% row.select %>
<% row.link :name, url: :admin_admin_user_path %>
<% row.text :email %>
<% row.boolean :credentials, label: "Passkey" do |cell| %>
<%= cell.value.any? ? "Yes" : "No" %>
<% end %>
<% end %>
<%= table_pagination_with(collection:) %>
9 changes: 4 additions & 5 deletions app/views/admin/admin_users/show.html+self.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
<%= builder.date :last_sign_in_at, label: { text: "Last sign in" } %>
<% end %>

<h3>Passkeys</h3>
<div class="repel">
<h3>Passkeys</h3>
<%= kpop_link_to "New passkey", new_admin_admin_user_credential_path(admin), class: "button button--primary" %>
</div>

<%= render "admin/credentials/credentials", admin: %>

<div class="actions-group">
<%= kpop_link_to "Add this device", new_admin_admin_user_credential_path(admin), class: "button button--primary" %>
</div>
10 changes: 10 additions & 0 deletions app/views/admin/credentials/_credentials.html+self.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<%= table_with(id: dom_id(admin, :credentials), collection: admin.credentials) do |t, c| %>
<% t.text :nickname, label: "Name" %>
<% t.date :updated_at, label: "Last use" do |date| %>
<%= date unless c.created_at == c.updated_at %>
<% end %>
<% t.cell :actions, label: "" do %>
<%= link_to("Remove passkey", admin_admin_user_credential_path(admin, c),
data: { turbo_method: :delete }) %>
<% end %>
<% end %>
9 changes: 4 additions & 5 deletions app/views/admin/credentials/_credentials.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<%= table_with(id: dom_id(admin, :credentials), collection: admin.credentials) do |t, c| %>
<% t.text :nickname %>
<% t.number :sign_count %>
<% t.cell :actions, label: "" do %>
<%= button_to "Remove device", admin_admin_user_credential_path(admin, c), method: :delete, class: "button button--text" %>
<% end if admin == current_admin %>
<% t.text :nickname, label: "Name" %>
<% t.date :updated_at, label: "Last use" do |date| %>
<%= date unless c.created_at == c.updated_at %>
<% end %>
<% end %>
31 changes: 27 additions & 4 deletions app/views/admin/credentials/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
<%= render Kpop::ModalComponent.new(title: "Register device") do %>
<%= render Kpop::ModalComponent.new(title: "New passkey") do %>
<%= form_with model: admin.credentials.new,
url: admin_admin_user_credentials_path(admin),
class: "flow prose",
data: {
controller: "webauthn-registration",
action: "submit->webauthn-registration#submit",
webauthn_registration_options_value: { publicKey: options },
} do |form| %>
<%= form.govuk_text_field :nickname %>
<%= form.hidden_field :response, data: { webauthn_registration_target: "response" } %>
<%= form.admin_save %>
<section class="flow prose" data-webauthn-registration-target="intro">
<p>
Passkeys are secure secrets that are stored by your device.
You will need the device where your passkey is stored to log in.
</p>
<p>
Unlike a password, your password doesn't get sent to the server when you log
in and can't be stolen in a data breach. When you log in with a passkey,
your operating system will prompt you for permission to use the passkey
secret to authenticate the login attempt.
</p>
<p>
We recommend that you store your passkey on your phone or cloud account.
Depending on your browser, you may need to choose "more options" to see
a QR code that you can scan with your phone.
</p>
</section>
<section class="flow" data-webauthn-registration-target="nickname" hidden>
<%= form.govuk_text_field :nickname, label: { text: "Passkey name" } do %>
Enter a name for this passkey to help you distinguish it from other passkeys you may have for this site.
<br>
Example: My Phone, Chrome, iCloud, 1Password
<% end %>
</section>
<%= form.admin_save("Next") %>
<% end %>
<% end %>

0 comments on commit c2d34a7

Please sign in to comment.