Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -230,7 +230,7 @@ Rails/I18nLocaleAssignment:
Enabled: true

Rails/I18nLocaleTexts:
Enabled: true
Enabled: false

Rails/IgnoredColumnsAssignment:
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ gem 'bootsnap', '~> 1.18', require: false
gem 'importmap-rails', '~> 2.2'
gem 'propshaft', '~> 1.2'
gem 'puma', '~> 7.0'
gem 'requestjs-rails'
gem "rollbar", "~> 3.6"
gem 'sqlite3', '>= 1.4'
gem 'stimulus-rails', '~> 1.3'
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ GEM
regexp_parser (2.11.2)
reline (0.6.2)
io-console (~> 0.5)
requestjs-rails (0.0.13)
railties (>= 7.1.0)
rexml (3.4.1)
rollbar (3.6.2)
rubocop (1.80.2)
Expand Down Expand Up @@ -342,6 +344,7 @@ DEPENDENCIES
puma (~> 7.0)
rack-mini-profiler (~> 4.0)
rails (~> 8.0.1)
requestjs-rails
rollbar (~> 3.6)
rubocop (~> 1.80)
rubocop-rails (~> 2.33)
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
20 changes: 13 additions & 7 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 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,16 +29,18 @@ 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
render json: { message: "Security Key registered successfully", redirect_to: root_path }, status: :ok
else
render json: "Couldn't add your Security Key", status: :unprocessable_content
render json: { message: "Couldn't add your Security Key", redirect_to: credentials_path },
status: :unprocessable_content
end
rescue WebAuthn::Error => e
render json: "Verification failed: #{e.message}", status: :unprocessable_content
render json: { message: "Verification failed: #{e.message}", redirect_to: credentials_path },
status: :unprocessable_content
ensure
session.delete(:current_registration)
end
Expand All @@ -51,4 +53,8 @@ def destroy

redirect_to root_path
end

def credential_params
params.expect(credential: [:public_key_credential, :nickname])
end
end
25 changes: 16 additions & 9 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 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,29 @@ 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
render json: { message: "Security Key registered successfully", redirect_to: root_path },
status: :ok
else
render json: "Couldn't register your Security Key", status: :unprocessable_content
render json: { message: "Couldn't register your Security Key", redirect_to: registration_path },
status: :unprocessable_content
end
rescue WebAuthn::Error => e
render json: "Verification failed: #{e.message}", status: :unprocessable_content
render json: { message: "Verification failed: #{e.message}", redirect_to: registration_path },
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm getting an error here – we should be using new_registration_path instead

status: :unprocessable_content
ensure
session.delete(:current_registration)
end
end

def registration_params
params.expect(registration: [:username, :nickname, :public_key_credential])
end
end
13 changes: 7 additions & 6 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,7 @@ class SessionsController < ApplicationController
def new
end

def create
def options
user = User.find_by(username: session_params[:username])

if user
Expand All @@ -25,8 +25,8 @@ def create
end
end

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,9 +44,10 @@ def callback
credential.update!(sign_count: webauthn_credential.sign_count)
sign_in(user)

render json: { status: "ok" }, status: :ok
render json: { message: "Security Key authenticated successfully", redirect_to: root_path }, status: :ok
rescue WebAuthn::Error => e
render json: "Verification failed: #{e.message}", status: :unprocessable_content
render json: { message: "Verification failed: #{e.message}", redirect_to: session_path },
Copy link
Contributor

@santiagorodriguez96 santiagorodriguez96 Oct 13, 2025

Choose a reason for hiding this comment

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

Same here! We should use new_session_path here instead

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not getting any of the messages rendered in the page 😕

status: :unprocessable_content
ensure
session.delete(:current_authentication)
end
Expand All @@ -61,6 +62,6 @@ def destroy
private

def session_params
params.require(:session).permit(:username)
params.expect(session: [:username, :public_key_credential])
end
end
2 changes: 1 addition & 1 deletion app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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";
import "@rails/request.js"

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.

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

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

async create() {
try {
const response = await fetch(this.optionsUrlValue, {
method: "POST",
body: new FormData(this.element),
});

const credentialOptionsJson = await response.json();
console.log(credentialOptionsJson);

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

const credential = await navigator.credentials.create({ publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(credentialOptionsJson) });
this.hiddenCredentialInputTarget.value = JSON.stringify(credential);

const submitResponse = await fetch(this.submitUrlValue, {
method: "POST",
body: new FormData(this.element),
});

const submitResponseJson = await submitResponse.json();

const { redirect_to } = submitResponseJson;

window.location.replace(redirect_to || "/");
} else {
showMessage(credentialOptionsJson.errors?.[0] || "Sorry, something wrong happened.");
this.submitButtonTarget.disabled = false;
}
} catch (error) {
showMessage(error.message || "Sorry, something wrong happened.");
this.submitButtonTarget.disabled = false;
}
}

async get() {
try {
const response = await fetch(this.optionsUrlValue, {
method: "POST",
body: new FormData(this.element),
});

const credentialOptionsJson = await response.json();
console.log(credentialOptionsJson);

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

const credential = await navigator.credentials.get({ publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(credentialOptionsJson) })
this.hiddenCredentialInputTarget.value = JSON.stringify(credential);

const submitResponse = await fetch(this.submitUrlValue, {
method: "POST",
body: new FormData(this.element),
});

const submitResponseJson = await submitResponse.json();

const { redirect_to } = submitResponseJson;

window.location.replace(redirect_to || "/");
} else {
showMessage(credentialOptionsJson.errors?.[0] || "Sorry, something wrong happened.");
this.submitButtonTarget.disabled = false;
}
} catch (error) {
showMessage(error.message || "Sorry, something wrong happened.");
this.submitButtonTarget.disabled = false;
}
}
}
Loading