Connect a NeoPass to an existing account

including validation logic to make sure it's not already connected to
another one!

The `intent` param on the NeoPass form is part of the key! Thanks
OmniAuth for making it easy to pass that data through!
This commit is contained in:
Emi Matchu 2024-04-08 05:33:58 -07:00
parent 09bccd41da
commit 5cc219c795
6 changed files with 100 additions and 9 deletions

View file

@ -1,8 +1,31 @@
class Devise::OmniauthCallbacksController < ApplicationController class Devise::OmniauthCallbacksController < ApplicationController
rescue_from AuthUser::AuthAlreadyConnected, with: :auth_already_connected
rescue_from AuthUser::MissingAuthInfoError, with: :missing_auth_info rescue_from AuthUser::MissingAuthInfoError, with: :missing_auth_info
rescue_from ActiveRecord::RecordInvalid, with: :validation_failed rescue_from ActiveRecord::RecordInvalid, with: :validation_failed
def neopass def neopass
case intent_param
when "login"
sign_in_with_neopass
when "connect"
connect_with_neopass
else
flash[:alert] = "Hrm, the NeoPass form you used was missing some " +
"\"intent\" information. That's surprising! Maybe try again?"
redirect_to root_path
end
end
def failure
flash[:warning] =
"Hrm, something went wrong in our connection to NeoPass, sorry! " +
"Maybe wait a moment and try again?"
redirect_to new_auth_user_session_path
end
private
def sign_in_with_neopass
@auth_user = AuthUser.from_omniauth(request.env["omniauth.auth"]) @auth_user = AuthUser.from_omniauth(request.env["omniauth.auth"])
if @auth_user.previously_new_record? if @auth_user.previously_new_record?
@ -18,11 +41,20 @@ class Devise::OmniauthCallbacksController < ApplicationController
sign_in_and_redirect @auth_user, event: :authentication sign_in_and_redirect @auth_user, event: :authentication
end end
def failure def connect_with_neopass
flash[:warning] = raise AccessDenied unless auth_user_signed_in?
"Hrm, something went wrong in our connection to NeoPass, sorry! " +
"Maybe wait a moment and try again?" current_auth_user.connect_omniauth!(request.env["omniauth.auth"])
redirect_to new_auth_user_session_path
flash[:notice] = "We've successfully connected your NeoPass!"
redirect_to edit_auth_user_path
end
def auth_already_connected(error)
flash[:alert] = "This NeoPass is already connected to another account. " +
"You'll need to log out of this account, log in with that NeoPass, " +
"then disconnect it from the other account."
redirect_to return_path
end end
def missing_auth_info(error) def missing_auth_info(error)
@ -33,7 +65,7 @@ class Devise::OmniauthCallbacksController < ApplicationController
"Hrm, our connection with NeoPass didn't return the information we " + "Hrm, our connection with NeoPass didn't return the information we " +
"usually expect, sorry! If this keeps happening, please email me at " + "usually expect, sorry! If this keeps happening, please email me at " +
"matchu@openneo.net so I can help investigate!" "matchu@openneo.net so I can help investigate!"
redirect_to new_auth_user_session_path redirect_to return_path
end end
def validation_failed(error) def validation_failed(error)
@ -44,6 +76,21 @@ class Devise::OmniauthCallbacksController < ApplicationController
"Hrm, the connection with NeoPass worked, but we had trouble saving " + "Hrm, the connection with NeoPass worked, but we had trouble saving " +
"your account, sorry! If this keeps happening, please email me at " + "your account, sorry! If this keeps happening, please email me at " +
"matchu@openneo.net so I can help investigate!" "matchu@openneo.net so I can help investigate!"
redirect_to new_auth_user_session_path redirect_to return_path
end
def intent_param
request.env['omniauth.params']["intent"]
end
def return_path
case intent_param
when "login"
new_auth_user_session_path
when "connect"
edit_auth_user_path
else
root_path
end
end end
end end

View file

@ -8,6 +8,8 @@ class AuthUser < AuthRecord
validates :name, presence: true, uniqueness: {case_sensitive: false}, validates :name, presence: true, uniqueness: {case_sensitive: false},
length: {maximum: 30} length: {maximum: 30}
validates :uid, uniqueness: {scope: :provider, allow_nil: true}
has_one :user, foreign_key: :remote_id, inverse_of: :auth_user has_one :user, foreign_key: :remote_id, inverse_of: :auth_user
# If the email is blank, ensure that it's `nil` rather than an empty string, # If the email is blank, ensure that it's `nil` rather than an empty string,
@ -79,6 +81,23 @@ class AuthUser < AuthRecord
encrypted_password? encrypted_password?
end end
def connect_omniauth!(auth)
raise MissingAuthInfoError, "Email missing" if auth.info.email.blank?
begin
update!(provider: auth.provider, uid: auth.uid,
neopass_email: auth.info.email)
rescue ActiveRecord::RecordInvalid
# If this auth is already bound to another account, raise a specific
# error about it, instead of the normal error.
if errors.where(:uid).any? { |e| e.type == :taken }
raise AuthAlreadyConnected, "there's already an account with " +
"provider #{auth.provider}, uid #{auth.uid}"
end
raise
end
end
def disconnect_neopass def disconnect_neopass
# If there's no NeoPass, we're already done! # If there's no NeoPass, we're already done!
return true if !uses_neopass? return true if !uses_neopass?
@ -100,6 +119,10 @@ class AuthUser < AuthRecord
end end
end end
def self.find_by_omniauth(auth)
find_by(provider: auth.provider, uid: auth.uid)
end
def self.from_omniauth(auth) def self.from_omniauth(auth)
raise MissingAuthInfoError, "Email missing" if auth.info.email.blank? raise MissingAuthInfoError, "Email missing" if auth.info.email.blank?
@ -160,5 +183,6 @@ class AuthUser < AuthRecord
"\"#{base_name}\" are taken??)" "\"#{base_name}\" are taken??)"
end end
class AuthAlreadyConnected < ArgumentError;end
class MissingAuthInfoError < ArgumentError;end class MissingAuthInfoError < ArgumentError;end
end end

View file

@ -96,6 +96,20 @@
<%= form.submit "Disconnect your NeoPass", <%= form.submit "Disconnect your NeoPass",
disabled: !@auth_user.uses_password? && !@auth_user.email? %> disabled: !@auth_user.uses_password? && !@auth_user.email? %>
<% end %> <% end %>
<% else %>
<%= form_with url: auth_user_neopass_omniauth_authorize_path(intent: "connect"),
method: :post, class: "settings-form", data: {turbo: false} do |form|
%>
<h2>Your NeoPass</h2>
<section class="neopass-explanation">
<p>
If you connect a NeoPass, you can use it to log into this DTI account!
You'll still be able to use your password to log in too, and you can
disconnect this later if you'd like.
</p>
</section>
<%= form.submit "Connect your NeoPass" %>
<% end %>
<% end %> <% end %>
<% content_for :stylesheets do %> <% content_for :stylesheets do %>

View file

@ -4,7 +4,7 @@
🌟✨🌟✨🌟✨🌟✨🌟 🌟✨🌟✨🌟✨🌟✨🌟
<br /> <br />
<%= button_to "Log in with NeoPass", <%= button_to "Log in with NeoPass",
auth_user_neopass_omniauth_authorize_path, auth_user_neopass_omniauth_authorize_path(intent: "login"),
data: {turbo: false} # Turbo can't handle this redirect! data: {turbo: false} # Turbo can't handle this redirect!
%> %>
🌟✨🌟✨🌟✨🌟✨🌟 🌟✨🌟✨🌟✨🌟✨🌟

View file

@ -0,0 +1,5 @@
class AddUniqueIndexForOmniauthToUsers < ActiveRecord::Migration[7.1]
def change
add_index :users, [:provider, :uid], unique: true
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_04_07_135246) do ActiveRecord::Schema[7.1].define(version: 2024_04_08_120359) do
create_table "users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t| create_table "users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t|
t.string "name", limit: 30, null: false t.string "name", limit: 30, null: false
t.string "encrypted_password", limit: 64 t.string "encrypted_password", limit: 64
@ -33,6 +33,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_04_07_135246) do
t.string "uid" t.string "uid"
t.string "neopass_email" t.string "neopass_email"
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
t.index ["provider", "uid"], name: "index_users_on_provider_and_uid", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
end end