Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ac998c2
feat: remove credential.js
rafaella-martino Aug 11, 2025
1532925
feat: remove ajax, rename controllers actions, use js alerts to displ…
rafaella-martino Aug 13, 2025
cb11999
feat: remove `message.js` as it is no longer used
rafaella-martino Aug 13, 2025
521b0c2
feat: remove unused js targets
rafaella-martino Aug 13, 2025
7bddc67
feat: replace `fetch` for `submit`, change `credential` param name to…
rafaella-martino Aug 13, 2025
871ab25
test: update test with renamed params
rafaella-martino Aug 14, 2025
cbf503e
feat: change `.then` for `await` for readability
rafaella-martino Aug 14, 2025
a990e01
feat: add permitted params in controllers
rafaella-martino Aug 14, 2025
bd72e31
feat: pass url as values, add token to session js controller
rafaella-martino Aug 15, 2025
6115812
feat: merge js controllers into one
rafaella-martino Aug 15, 2025
e19ef24
feat: change submit buttons, rename js controller variables
rafaella-martino Aug 15, 2025
9b46e85
fix: attend deprecation warning
rafaella-martino Aug 15, 2025
6fe61ca
Merge branch 'master' into rm--refactor_js
rafaella-martino Aug 15, 2025
6002921
style: use rails button helpers
rafaella-martino Aug 15, 2025
e93e3e8
feat: use `expect` to require params
rafaella-martino Aug 15, 2025
95b5664
feat: change `create_options` and `get_options` controller actions for
rafaella-martino Aug 18, 2025
331859e
fix: add space in css
rafaella-martino Aug 18, 2025
c42c9fc
rollback: use messenger.js to display error messages
rafaella-martino Aug 18, 2025
7a35c2c
feat: use `requestjs` gem
rafaella-martino Aug 18, 2025
81ad153
fix: Display layout instead of json when Webauthn Verification error
rafaella-martino Aug 18, 2025
c2753ed
feat: use render instead of redirect when request fails, use flash for
rafaella-martino Aug 19, 2025
1c92613
feat: Change variable names in JS controller
rafaella-martino Aug 19, 2025
5307cb1
refactor: move `request.js` import with the rest of the imports
rafaella-martino Sep 3, 2025
449c9cb
refactor: temporarily use `redirect` when `create` credential fails,
rafaella-martino Sep 3, 2025
e8b7ea3
refactor: enabling `submit` button when clicking `Cancel` with js
rafaella-martino Sep 10, 2025
9e58703
feat: clear not saved credential if there is an error
rafaella-martino Sep 10, 2025
672bcb1
Merge branch 'master' into rm--refactor_js
rafaella-martino Sep 11, 2025
b9edbe2
make rubocop happy
rafaella-martino Sep 11, 2025
23a316e
refactor: remove unnecessary cleanup when error updating credential
rafaella-martino Sep 11, 2025
55e10a6
rollback handling Webauthn Error exception
rafaella-martino Sep 11, 2025
e35488d
refactor: rollback to use json responses in case of errors
rafaella-martino Sep 12, 2025
3accfb1
refactor: leave controller response for next PR, rollback to respond
rafaella-martino Sep 12, 2025
18b76a5
change response message if registration success
rafaella-martino Sep 12, 2025
72e5f40
test: add new controller changes
rafaella-martino Sep 12, 2025
82a766f
stop displaying message
rafaella-martino Sep 12, 2025
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
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ Rails/I18nLocaleAssignment:
Enabled: true

Rails/I18nLocaleTexts:
Enabled: true
Enabled: false

Rails/IgnoredColumnsAssignment:
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ body {

.center {
display: flex;
justify-content:center;
}

.center input {
Expand Down
16 changes: 10 additions & 6 deletions app/controllers/credentials_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class CredentialsController < ApplicationController
def create
def create_options
create_options = WebAuthn::Credential.options_for_create(
user: {
id: current_user.webauthn_id,
Expand All @@ -18,8 +18,8 @@ def create
end
end

def callback
webauthn_credential = WebAuthn::Credential.from_create(params)
def create
webauthn_credential = WebAuthn::Credential.from_create(JSON.parse(credential_params[:public_key_credential]))

begin
webauthn_credential.verify(session[:current_registration]["challenge"], user_verification: true)
Expand All @@ -29,13 +29,13 @@ def callback
)

if credential.update(
nickname: params[:credential_nickname],
nickname: credential_params[:nickname],
public_key: webauthn_credential.public_key,
sign_count: webauthn_credential.sign_count
)
render json: { status: "ok" }, status: :ok
redirect_to root_path, notice: "Security Key registered successfully"
else
render json: "Couldn't add your Security Key", status: :unprocessable_content
redirect_to root_path, alert: "Couldn't register your Security Key"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

end
rescue WebAuthn::Error => e
render json: "Verification failed: #{e.message}", status: :unprocessable_content
Expand All @@ -51,4 +51,8 @@ def destroy

redirect_to root_path
end

def credential_params
params.require(:credential).permit(:public_key_credential, :nickname)
end
end
20 changes: 12 additions & 8 deletions app/controllers/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ class RegistrationsController < ApplicationController
def new
end

def create
user = User.new(username: params[:registration][:username])
def create_options
user = User.new(username: registration_params[:username])

create_options = WebAuthn::Credential.options_for_create(
user: {
name: params[:registration][:username],
name: registration_params[:username],
id: user.webauthn_id
},
authenticator_selection: { user_verification: "required" }
Expand All @@ -28,8 +28,8 @@ def create
end
end

def callback
webauthn_credential = WebAuthn::Credential.from_create(params)
def create
webauthn_credential = WebAuthn::Credential.from_create(JSON.parse(registration_params[:public_key_credential]))

user = User.new(session[:current_registration]["user_attributes"])

Expand All @@ -38,22 +38,26 @@ def callback

user.credentials.build(
external_id: webauthn_credential.id,
nickname: params[:credential_nickname],
nickname: registration_params[:nickname],
public_key: webauthn_credential.public_key,
sign_count: webauthn_credential.sign_count
)

if user.save
sign_in(user)

render json: { status: "ok" }, status: :ok
redirect_to root_path, notice: "Security Key registered successfully"
else
render json: "Couldn't register your Security Key", status: :unprocessable_content
redirect_to new_registration_path, alert: "Couldn't register your Security Key"
Copy link
Member

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

end
rescue WebAuthn::Error => e
render json: "Verification failed: #{e.message}", status: :unprocessable_content
ensure
session.delete(:current_registration)
end
end

def registration_params
params.require(:registration).permit(:username, :nickname, :public_key_credential)
end
end
12 changes: 7 additions & 5 deletions app/controllers/sessions_controller.rb
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when the sign in fails it renders:

Image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ class SessionsController < ApplicationController
def new
end

def create
# rubocop:disable Naming/AccessorMethodName
def get_options
user = User.find_by(username: session_params[:username])

if user
Expand All @@ -24,9 +25,10 @@ def create
end
end
end
# rubocop:enable Naming/AccessorMethodName

def callback
webauthn_credential = WebAuthn::Credential.from_get(params)
def create
webauthn_credential = WebAuthn::Credential.from_get(JSON.parse(session_params[:public_key_credential]))

user = User.find_by(username: session[:current_authentication]["username"])
raise "user #{session[:current_authentication]["username"]} never initiated sign up" unless user
Expand All @@ -44,7 +46,7 @@ def callback
credential.update!(sign_count: webauthn_credential.sign_count)
sign_in(user)

render json: { status: "ok" }, status: :ok
redirect_to root_path, notice: "Security Key authenticated successfully"
rescue WebAuthn::Error => e
render json: "Verification failed: #{e.message}", status: :unprocessable_content
ensure
Expand All @@ -61,6 +63,6 @@ def destroy
private

def session_params
params.require(:session).permit(:username)
params.require(:session).permit(:username, :public_key_credential)
end
end
2 changes: 0 additions & 2 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "controllers"
import "credential"
import "messenger"
import Rails from "@rails/ujs";

Rails.start();
13 changes: 0 additions & 13 deletions app/javascript/controllers/add_credential_controller.js

This file was deleted.

29 changes: 0 additions & 29 deletions app/javascript/controllers/new_registration_controller.js

This file was deleted.

22 changes: 0 additions & 22 deletions app/javascript/controllers/new_session_controller.js

This file was deleted.

60 changes: 60 additions & 0 deletions app/javascript/controllers/webauthn_credential_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["hiddenCredentialInput"]
static values = { optionsUrl: String }

async create() {
try {
const optionsResponse = await fetch(this.optionsUrlValue, {
method: "POST",
body: new FormData(this.element),
headers: {
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')?.getAttribute("content")
}
});

const optionsJson = await optionsResponse.json();
console.log(optionsJson);

if (optionsResponse.ok) {
console.log("Creating new public key credential...");

const credential = await navigator.credentials.create({ publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson) });
this.hiddenCredentialInputTarget.value = JSON.stringify(credential);
this.element.submit();
} else {
alert(optionsJson.errors?.[0] || "Sorry, something wrong happened.");
}
} catch (error) {
alert(error.message || error);
}
}

async get() {
try {
const optionsResponse = await fetch(this.optionsUrlValue, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about naming it response?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1c92613
Like it, I also changed optionsJson to credentialOptionsJson.
Are you okay with this change or is it too long?
As this is a Demo I believe variables should be understandable 🙂

method: "POST",
body: new FormData(this.element),
headers: {
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')?.getAttribute("content")
}
});

const optionsJson = await optionsResponse.json();
console.log(optionsJson);

if (optionsResponse.ok) {
console.log("Getting public key credential...");

const credential = await navigator.credentials.get({ publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson) })
this.hiddenCredentialInputTarget.value = JSON.stringify(credential);
this.element.submit();
} else {
alert(optionsJson.errors?.[0] || "Sorry, something wrong happened.");
}
} catch (error) {
alert(error.message || error);
}
}
}
57 changes: 0 additions & 57 deletions app/javascript/credential.js

This file was deleted.

11 changes: 0 additions & 11 deletions app/javascript/messenger.js

This file was deleted.

12 changes: 10 additions & 2 deletions app/views/home/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,24 @@
</ul>

<div data-controller="add-credential">
<%= form_with scope: :credential, url: credentials_path, local: false, data: { action: "ajax:success->add-credential#create" } do |form| %>
<%= form_with(
scope: :credential,
url: credentials_path,
data: {
controller: "webauthn-credential",
action: "webauthn-credential#create:prevent",
"webauthn-credential-options-url-value": create_options_credentials_path,
}) do |form| %>
<div class="form-field">
<div class="mdc-text-field mdc-text-field--fullwidth" data-controller="textfield">
<%= form.text_field :nickname, class: "mdc-text-field__input", placeholder: "New Security Key nickname", required: true %>
<div class="mdc-line-ripple"></div>
</div>
<%= form.hidden_field :public_key_credential, data: { "webauthn-credential-target": "hiddenCredentialInput" } %>
</div>

<div class="center">
<%= form.submit "Add Security Key", class: "mdc-button mdc-button--unelevated" %>
<button type="submit" class="mdc-button mdc-button--unelevated">Add Security Key</button>
</div>
<% end %>
</div>
Expand Down
Loading