Module: Devise::Passkeys::Controllers::RegistrationsControllerConcern

Extended by:
ActiveSupport::Concern
Defined in:
lib/devise/passkeys/controllers/registrations_controller_concern.rb

Overview

This concern should be included in any controller that handles user (resource) registration management (ie: signup/deleting an account), and defines:

  • Useful methods and before filters to streamline user (resource) registration management using session variables
  • Controller actions for:
    • Issuing a new WebAuthn challenge
    • A create action that creates a passkey if the user (resource) has been persisted
  • Helper modules from Warden::WebAuthn that are required to complete the registration process

The registration_user_id_key and registration_challenge_key are defined using the resource_name, to keep the generated IDs unique between resources during the registration process.

A raw_credential method is provided to streamline access to passkey_params[:passkey_credential].

Note: the implementing controller must define a relying_party method in order for registrations to work.

Examples:

class RegistrationsController < ApplicationController
  include Devise::Passkeys::Controllers::RegistrationsControllerConcern

  def relying_party
    WebAuthn::RelyingParty.new
  end
end

See Also:

Instance Method Summary collapse

Instance Method Details

#configure_sign_up_paramsObject

Adds the generated WebAuthn User ID to devise_parameter_sanitizer's permitted keys



252
253
254
255
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 252

def 
  params[resource_name][:webauthn_id] = registration_user_id
  devise_parameter_sanitizer.permit(:sign_up, keys: [:webauthn_id])
end

#createObject

This controller action creates a new user (resource), using the given email & passkey. It:

  1. calls the parent class's #create method
  2. calls #create_passkey_for_resource to finish creating the passkey if the user (resource) was actually persisted
  3. Finishes the rest of the parent class's #create method

The following before actions are called:

  • require_email_and_passkey_label
  • verify_passkey_registration_challenge
  • configure_sign_up_params


110
111
112
113
114
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 110

def create
  super do |resource|
    create_passkey_for_resource(resource: resource)
  end
end

#create_passkey(resource:) ⇒ Object

Generates a passkey for the given resource, using the resource.passkeys.create! method with the following attributes:

  • label: The passkey_params[:passkey_label]
  • public_key: The @webauthn_credential.public_key
  • external_id: The credential ID, strictly encoded as a Base 64 string
  • sign_count: The @webauthn_credential.sign_count
  • last_used_at: The current time, since this is the first time the passkey is being used


152
153
154
155
156
157
158
159
160
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 152

def create_passkey(resource:)
  resource.passkeys.create!(
    label: passkey_params[:passkey_label],
    public_key: @webauthn_credential.public_key,
    external_id: Base64.strict_encode64(@webauthn_credential.raw_id),
    sign_count: @webauthn_credential.sign_count,
    last_used_at: Time.now.utc
  )
end

#create_passkey_for_resource(resource:) {|resource, passkey| ... } ⇒ Object

Creates a passkey for given user (resource).

The method tests that the user (resource) is in the database before saving the passkey for the given user (resource).

This method also ensures that the generated WebAuthn User ID is deleted from the session to prevent data leaks.

Yields:

  • (resource, passkey)

    The provided resource and the newly created passkey.

See Also:



132
133
134
135
136
137
138
139
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 132

def create_passkey_for_resource(resource:)
  return unless resource.persisted?

  passkey = create_passkey(resource: resource)

  yield [resource, passkey] if block_given?
  delete_registration_user_id!
end

#exclude_external_ids_for_registrationObject

Override this method if you need to exclude certain WebAuthn credentials from a registration request.



200
201
202
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 200

def exclude_external_ids_for_registration
  []
end

#new_challengeObject

This controller action issues a new challenge for the registration handshake.

The challenge is stored in a session variable, and renders the WebAuthn registration options as a JSON response.

The following before filters are called:

  • require_no_authentication
  • require_email_and_passkey_label

See Also:



79
80
81
82
83
84
85
86
87
88
89
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 79

def new_challenge
  options_for_registration = generate_registration_options(
    relying_party: relying_party,
    user_details: user_details_for_registration,
    exclude: exclude_external_ids_for_registration
  )

  store_challenge_in_session(options_for_registration: options_for_registration)

  render json: options_for_registration
end

#passkey_paramsObject

The subset of parameters used when verifying the passkey



206
207
208
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 206

def passkey_params
  params.require(resource_name).permit(:passkey_label, :passkey_credential)
end

#reauthentication_paramsObject

The subset of parameters used when verifying a reauthentication_token



183
184
185
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 183

def reauthentication_params
  params.require(resource_name).permit(:reauthentication_token)
end

#require_email_and_passkey_labelObject

Verifies that the sign_up_params has an :email and :passkey_label.

If either is missing or blank, a 400 Bad Request JSON response is rendered.

Examples:

{"error": "Please enter your email address."}


217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 217

def require_email_and_passkey_label
  if [:email].blank?
    render json: { message: find_message(:email_missing) }, status: :bad_request
    return false
  end

  if passkey_params[:passkey_label].blank?
    render json: { message: find_message(:passkey_label_missing) }, status: :bad_request
    return false
  end

  true
end

#update_resource(resource, params) ⇒ Object

An override of DeviseController's implementation, to circumvent the update_with_password method

See Also:

  • DeviseController#update_resource


191
192
193
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 191

def update_resource(resource, params)
  resource.update(params)
end

#user_details_for_registrationObject

Prepares the user details for a WebAuthn registration request



261
262
263
264
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 261

def user_details_for_registration
  store_registration_user_id
  { id: registration_user_id, name: [:email] }
end

#verify_passkey_registration_challengeObject

Verifies the registration challenge is correct.

If the challenge failed, a 400 Bad Request JSON response is rendered.

Examples:

{"error": "Please try a different passkey."}

See Also:



243
244
245
246
247
248
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 243

def verify_passkey_registration_challenge
  @webauthn_credential = verify_registration(relying_party: relying_party)
rescue ::WebAuthn::Error => e
  error_key = Warden::WebAuthn::ErrorKeyFinder.webauthn_error_key(exception: e)
  render json: { message: find_message(error_key) }, status: :bad_request
end

#verify_reauthentication_tokenObject

Verifies that the given reauthentication token matches the expected value stored in the session.

If the reauthentication token is not valid, a 400 Bad Request JSON response is rendered.

Examples:

{"error": "Please reauthenticate to continue."}

See Also:



175
176
177
178
179
# File 'lib/devise/passkeys/controllers/registrations_controller_concern.rb', line 175

def verify_reauthentication_token
  return if valid_reauthentication_token?(given_reauthentication_token: reauthentication_params[:reauthentication_token])

  render json: { error: find_message(:not_reauthenticated) }, status: :bad_request
end