|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +class WebauthnCredentialsController < ApplicationController |
| 4 | + include CommonBehavior |
| 5 | + |
| 6 | + before_action :set_user_and_authorize |
| 7 | + before_action :set_webauthn_credential, only: MEMBER_ACTIONS |
| 8 | + |
| 9 | + def show; end |
| 10 | + |
| 11 | + def new |
| 12 | + @webauthn_credential = WebauthnCredential.new(user: @user) |
| 13 | + authorize @webauthn_credential |
| 14 | + |
| 15 | + @webauthn_create_options = personalized_options |
| 16 | + session[:current_challenge] = @webauthn_create_options.challenge |
| 17 | + end |
| 18 | + |
| 19 | + def edit; end |
| 20 | + |
| 21 | + def create |
| 22 | + @webauthn_credential = @user.webauthn_credentials.build( |
| 23 | + label: webauthn_credential_params[:label] |
| 24 | + ) |
| 25 | + |
| 26 | + authorize! |
| 27 | + |
| 28 | + raise WebAuthn::Error.new(t('.missing_challenge')) unless session[:current_challenge] |
| 29 | + raise WebAuthn::Error.new(t('.invalid_param')) unless credential_param.is_a?(Hash) && credential_param.key?('rawId') |
| 30 | + |
| 31 | + credential = WebAuthn::Credential.from_create(credential_param) |
| 32 | + credential.verify(session[:current_challenge], user_presence: true, user_verification: true) |
| 33 | + |
| 34 | + @webauthn_credential.assign_attributes( |
| 35 | + external_id: credential.id, |
| 36 | + public_key: credential.public_key, |
| 37 | + sign_count: credential.sign_count, |
| 38 | + transports: credential.response.transports |
| 39 | + ) |
| 40 | + |
| 41 | + # In case something goes wrong, we want to show the user the same options again. |
| 42 | + @webauthn_create_options = personalized_options |
| 43 | + session[:current_challenge] = @webauthn_create_options.challenge |
| 44 | + |
| 45 | + create_and_respond(object: @webauthn_credential, path: -> { @webauthn_credential.user }) do |
| 46 | + session.delete(:current_challenge) |
| 47 | + # Don't return a specific value from this block, so that the default is used. |
| 48 | + nil |
| 49 | + end |
| 50 | + rescue JSON::ParserError, WebAuthn::Error => e |
| 51 | + flash.now[:danger] = ERB::Util.html_escape e.message |
| 52 | + respond_to do |format| |
| 53 | + @webauthn_create_options = personalized_options |
| 54 | + session[:current_challenge] = @webauthn_create_options.challenge |
| 55 | + |
| 56 | + respond_with_invalid_object(format, template: :new) |
| 57 | + end |
| 58 | + end |
| 59 | + |
| 60 | + def update |
| 61 | + update_and_respond(object: @webauthn_credential, params: {label: webauthn_credential_params[:label]}, path: [@webauthn_credential.user, @webauthn_credential]) |
| 62 | + end |
| 63 | + |
| 64 | + def destroy |
| 65 | + destroy_and_respond(object: @webauthn_credential, path: @webauthn_credential.user) |
| 66 | + end |
| 67 | + |
| 68 | + private |
| 69 | + |
| 70 | + def personalized_options |
| 71 | + @user.with_lock do |
| 72 | + if @user.webauthn_user_id.blank? |
| 73 | + @user.validate_password = false if @user.respond_to?(:validate_password=) |
| 74 | + @user.update!(webauthn_user_id: WebAuthn.generate_user_id) |
| 75 | + end |
| 76 | + end |
| 77 | + |
| 78 | + WebAuthn::Credential.options_for_create( |
| 79 | + user: { |
| 80 | + id: @user.webauthn_user_id, |
| 81 | + display_name: @user.displayname, |
| 82 | + name: @user.webauthn_name, |
| 83 | + }, |
| 84 | + exclude_credentials:, |
| 85 | + authenticator_selection: { |
| 86 | + user_verification: :required, |
| 87 | + } |
| 88 | + ) |
| 89 | + end |
| 90 | + |
| 91 | + def exclude_credentials |
| 92 | + @user.webauthn_credentials.map do |cred| |
| 93 | + {id: cred.external_id, type: WebAuthn::TYPE_PUBLIC_KEY, transports: cred.transports} |
| 94 | + end |
| 95 | + end |
| 96 | + |
| 97 | + def authorize! |
| 98 | + raise Pundit::NotAuthorizedError if @webauthn_credential.present? && @user.present? && @webauthn_credential.user != @user |
| 99 | + |
| 100 | + authorize(@webauthn_credential) |
| 101 | + end |
| 102 | + |
| 103 | + def set_user_and_authorize |
| 104 | + if params[:external_user_id] |
| 105 | + @user = ExternalUser.find(params[:external_user_id]) |
| 106 | + else |
| 107 | + @user = InternalUser.find(params[:internal_user_id]) |
| 108 | + end |
| 109 | + params[:user_id] = @user.id_with_type # for the breadcrumbs |
| 110 | + authorize(@user, :update?) |
| 111 | + end |
| 112 | + |
| 113 | + def set_webauthn_credential |
| 114 | + @webauthn_credential = WebauthnCredential.find(params[:id]) |
| 115 | + authorize! |
| 116 | + end |
| 117 | + |
| 118 | + def webauthn_credential_params |
| 119 | + params.require(:webauthn_credential).permit(:credential, :label) |
| 120 | + end |
| 121 | + |
| 122 | + def credential_param |
| 123 | + return @credential_param if defined? @credential_param |
| 124 | + |
| 125 | + credential_param = webauthn_credential_params[:credential] |
| 126 | + @credential_param = JSON.parse(credential_param.to_s) |
| 127 | + rescue JSON::ParserError |
| 128 | + @credential_param = {} |
| 129 | + end |
| 130 | +end |
0 commit comments