Implement Passkey authentication
This commit is contained in:
@@ -8,6 +8,8 @@ module CurrentRequest
|
||||
Current.user_agent = request.user_agent
|
||||
Current.ip_address = request.ip
|
||||
Current.referrer = request.referrer
|
||||
|
||||
ActionPack::WebAuthn::Current.host = request.host
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
class Sessions::PasskeysController < ApplicationController
|
||||
disallow_account_scope
|
||||
require_unauthenticated_access
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: :rate_limit_exceeded
|
||||
|
||||
def create
|
||||
credential = Identity::Credential.authenticate(
|
||||
id: params.expect(:credential_id),
|
||||
client_data_json: response_params[:client_data_json],
|
||||
authenticator_data: response_params[:authenticator_data],
|
||||
signature: response_params[:signature],
|
||||
challenge: session.delete(:webauthn_challenge),
|
||||
origin: request.base_url
|
||||
)
|
||||
|
||||
if credential
|
||||
authentication_succeeded(credential.identity)
|
||||
else
|
||||
authentication_failed
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def response_params
|
||||
params.expect(response: [ :client_data_json, :authenticator_data, :signature ])
|
||||
end
|
||||
|
||||
def authentication_succeeded(identity)
|
||||
start_new_session_for identity
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to after_authentication_url }
|
||||
format.json { render json: { session_token: session_token } }
|
||||
end
|
||||
end
|
||||
|
||||
def authentication_failed
|
||||
alert_message = "That passkey didn't work. Try again."
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: alert_message }
|
||||
format.json { render json: { message: alert_message }, status: :unauthorized }
|
||||
end
|
||||
end
|
||||
|
||||
def rate_limit_exceeded
|
||||
rate_limit_exceeded_message = "Try again later."
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to new_session_path, alert: rate_limit_exceeded_message }
|
||||
format.json { render json: { message: rate_limit_exceeded_message }, status: :too_many_requests }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,8 @@ class SessionsController < ApplicationController
|
||||
layout "public"
|
||||
|
||||
def new
|
||||
@request_options = Identity::Credential.request_options
|
||||
session[:webauthn_challenge] = @request_options.challenge
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -67,4 +69,5 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,71 +1,40 @@
|
||||
class Users::CredentialsController < ApplicationController
|
||||
before_action :set_user
|
||||
before_action :set_webauthn_host
|
||||
before_action :set_credential, only: :destroy
|
||||
|
||||
def index
|
||||
@credentials = identity.credentials.order(created_at: :desc)
|
||||
@credentials = Current.identity.credentials.order(name: :asc, created_at: :desc)
|
||||
end
|
||||
|
||||
def new
|
||||
@creation_options = creation_options
|
||||
@creation_options = Identity::Credential.creation_options(identity: Current.identity, display_name: Current.user.name)
|
||||
session[:webauthn_challenge] = @creation_options.challenge
|
||||
end
|
||||
|
||||
def create
|
||||
public_key_credential = ActionPack::WebAuthn::PublicKeyCredential.create(
|
||||
client_data_json: decode64(credential_response[:client_data_json]),
|
||||
attestation_object: decode64(credential_response[:attestation_object]),
|
||||
Identity::Credential.register(
|
||||
identity: Current.identity,
|
||||
name: credential_params[:name],
|
||||
client_data_json: credential_params[:client_data_json],
|
||||
attestation_object: credential_params[:attestation_object],
|
||||
challenge: session.delete(:webauthn_challenge),
|
||||
origin: request.base_url,
|
||||
transports: Array(credential_response[:transports])
|
||||
transports: Array(credential_params[:transports])
|
||||
)
|
||||
|
||||
identity.credentials.create!(
|
||||
name: params.dig(:credential, :name),
|
||||
credential_id: public_key_credential.id,
|
||||
public_key: public_key_credential.public_key.to_der,
|
||||
sign_count: public_key_credential.sign_count,
|
||||
transports: public_key_credential.transports
|
||||
)
|
||||
|
||||
redirect_to user_credentials_path(@user)
|
||||
redirect_to user_credentials_path(Current.user)
|
||||
end
|
||||
|
||||
def destroy
|
||||
identity.credentials.find(params[:id]).destroy!
|
||||
redirect_to user_credentials_path(@user)
|
||||
@credential.destroy!
|
||||
redirect_to user_credentials_path(Current.user)
|
||||
end
|
||||
|
||||
private
|
||||
def set_user
|
||||
@user = Current.identity.users.find(params[:user_id])
|
||||
def set_credential
|
||||
@credential = Current.identity.credentials.find(params[:id])
|
||||
end
|
||||
|
||||
def set_webauthn_host
|
||||
ActionPack::WebAuthn::Current.host = request.host
|
||||
def credential_params
|
||||
params.expect(credential: [ :name, :client_data_json, :attestation_object, transports: [] ])
|
||||
end
|
||||
|
||||
def identity
|
||||
@user.identity
|
||||
end
|
||||
|
||||
def creation_options
|
||||
ActionPack::WebAuthn::PublicKeyCredential::CreationOptions.new(
|
||||
id: identity.id,
|
||||
name: identity.email_address,
|
||||
display_name: @user.name,
|
||||
resident_key: :required,
|
||||
exclude_credentials: identity.credentials.map { |c| ExcludeCredential.new(c.credential_id, c.transports) }
|
||||
)
|
||||
end
|
||||
|
||||
def credential_response
|
||||
params.expect(credential: { response: [ :client_data_json, :attestation_object, transports: [] ] })[:response]
|
||||
end
|
||||
|
||||
def decode64(value)
|
||||
Base64.urlsafe_decode64(value)
|
||||
end
|
||||
|
||||
ExcludeCredential = Struct.new(:id, :transports)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user