From 3eeb5d1065f9f3dd41ae16cd978eba66627c9dba Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Thu, 14 Mar 2024 19:11:06 -0700 Subject: [PATCH] Actually create user from NeoPass authentication! <3 <3 Whew, exciting! Still done nothing against the live NeoPass server, but we've got this fully working with the development server, it seems! Wowie!! This is all still hidden behind secret flags, so it's fine to deploy live. (And it's not actually a problem if someone gets past to the endpoints behind it, because we haven't actually set up real credentials for our NeoPass client yet, so authentication will fail!) Okay time to lie down lol. --- .../devise/omniauth_callbacks_controller.rb | 43 ++++++++++++++- app/models/auth_user.rb | 55 +++++++++++++++++++ bin/neopass-server | 13 +++-- ...allow_null_email_and_password_for_users.rb | 7 +++ db/openneo_id_schema.rb | 8 +-- 5 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 db/openneo_id_migrate/20240315020053_allow_null_email_and_password_for_users.rb diff --git a/app/controllers/devise/omniauth_callbacks_controller.rb b/app/controllers/devise/omniauth_callbacks_controller.rb index 98a607a8..23ff20eb 100644 --- a/app/controllers/devise/omniauth_callbacks_controller.rb +++ b/app/controllers/devise/omniauth_callbacks_controller.rb @@ -1,9 +1,48 @@ class Devise::OmniauthCallbacksController < ApplicationController + rescue_from AuthUser::MissingAuthInfoError, with: :missing_auth_info + rescue_from ActiveRecord::RecordInvalid, with: :validation_failed + def neopass - render plain: request.env["omniauth.auth"].uid + @auth_user = AuthUser.from_omniauth(request.env["omniauth.auth"]) + + if @auth_user.previously_new_record? + flash[:notice] = + "Welcome to Dress to Impress, #{@auth_user.name}! We've set up a DTI " + + "account for you. Click Settings in the top right to learn more, or " + + "just go ahead and get started!" + else + flash[:notice] = "Welcome back, #{@auth_user.name}! 💖" + end + + sign_in_and_redirect @auth_user, event: :authentication end def failure - render plain: "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 + + def missing_auth_info(error) + Sentry.capture_exception error + Rails.logger.error error.full_message + + flash[:warning] = + "Hrm, our connection with NeoPass didn't return the information we " + + "usually expect, sorry! If this keeps happening, please email me at " + + "matchu@openneo.net so I can help investigate!" + redirect_to new_auth_user_session_path + end + + def validation_failed(error) + Sentry.capture_exception error + Rails.logger.error error.full_message + + flash[:warning] = + "Hrm, the connection with NeoPass worked, but we had trouble saving " + + "your account, sorry! If this keeps happening, please email me at " + + "matchu@openneo.net so I can help investigate!" + redirect_to new_auth_user_session_path end end diff --git a/app/models/auth_user.rb b/app/models/auth_user.rb index 0dacacfe..89b4a226 100644 --- a/app/models/auth_user.rb +++ b/app/models/auth_user.rb @@ -27,4 +27,59 @@ class AuthUser < AuthRecord user.name = name user.save! end + + def uses_omniauth? + provider? && uid? + end + + def email_required? + !uses_omniauth? + end + + def password_required? + !uses_omniauth? + end + + def self.from_omniauth(auth) + raise MissingAuthInfoError, "Username missing" if auth.uid.blank? + raise MissingAuthInfoError, "Email missing" if auth.info.email.blank? + + transaction do + find_or_create_by!(provider: auth.provider, uid: auth.uid) do |user| + # Use the Neopets username if possible, or a unique username if not. + dti_username = build_unique_username_like(auth.uid) + user.name = dti_username + + # Copy the email address from their Neopets account to their DTI + # account, unless they already have a DTI account with this email, in + # which case, ignore it. (It's primarily for their own convenience with + # password recovery!) + email_exists = AuthUser.where(email: auth.info.email).exists? + user.email = auth.info.email unless email_exists + end + end + end + + def self.build_unique_username_like(name) + name_query = sanitize_sql_like(name) + "%" + similar_names = where("name LIKE ?", name_query).pluck(:name) + + # Use the given name itself, if we can. + return name unless similar_names.include?(name) + + # If not, try appending "-neopass". + return "#{name}-neopass" unless similar_names.include?("#{name}-neopass") + + # After that, try appending "-neopass-1", "-neopass-2", etc, until a + # unique name arises. (We don't expect this to happen basically ever, but + # it's nice to have a guarantee!) + max = similar_names.size + 1 + candidates = (1..max).map { |n| "#{name}-neopass-#{n}"} + numerical_name = candidates.find { |name| !similar_names.include?(name) } + return numerical_name unless numerical_name.nil? + + raise "Failed to build unique username (shouldn't be possible?)" + end + + class MissingAuthInfoError < ArgumentError;end end \ No newline at end of file diff --git a/bin/neopass-server b/bin/neopass-server index 6e61576d..ba0713b7 100755 --- a/bin/neopass-server +++ b/bin/neopass-server @@ -21,9 +21,10 @@ const urlLib = require("node:url"); const { OAuth2Server } = require("oauth2-mock-server"); const express = require("express"); -// This is the Neopets username we'll report back to DTI when you authenticate -// through here. -const USERNAME = "theneopetsteam"; +// This is the Neopets username and email we'll report back to DTI when you +// authenticate through here. +const USERNAME = "test"; +const EMAIL = "theneopetsteam@neopets.com"; const certPath = pathLib.join(__dirname, "..", "tmp", "localhost.pem"); const keyPath = pathLib.join(__dirname, "..", "tmp", "localhost-key.pem"); @@ -88,10 +89,14 @@ async function startServer(port) { n: "svVfGU4NGcfBCmQiIOW5uzg5SAN2CWSIQSstnhqZoCdjy5OoKpKVR8O9TbDvxixrvkFyAav90Q0Xse8iFTcjfCKuqINYiuYMXhCvfBlc_DVVOQca9pMpN03LaDofd5Ll4_BFTtt1nSPahwWU7xDM-Bkkh_TcS2qS4N2xbpEGi0q0ZkrJN4WyiDBC2k9WbK-YHr4Rj4JKypFVSeBIrjxVPmlPzgfqlLGGIB0l92SnJDXDMlkWcCCTyLgqSBM04nkxGDSykq_ei76qCdRd7b10wMBaoS9DeBThAyHpur2LoPdH3gxbcwoWExi-jPlNP1LdKVZD8b95OY3CRyMAAMGdKQ", }); - server.service.on("beforeTokenSigning", (token, req) => { + server.service.on("beforeTokenSigning", (token) => { token.payload.sub = USERNAME; }); + server.service.on("beforeUserinfo", (userInfoResponse) => { + userInfoResponse.body.email = EMAIL; + }); + await server.start(port, "localhost"); console.log(`Started NeoPass development server at: ${server.issuer.url}`); } diff --git a/db/openneo_id_migrate/20240315020053_allow_null_email_and_password_for_users.rb b/db/openneo_id_migrate/20240315020053_allow_null_email_and_password_for_users.rb new file mode 100644 index 00000000..eb3c79ea --- /dev/null +++ b/db/openneo_id_migrate/20240315020053_allow_null_email_and_password_for_users.rb @@ -0,0 +1,7 @@ +class AllowNullEmailAndPasswordForUsers < ActiveRecord::Migration[7.1] + def change + change_column_null :users, :email, true + change_column_null :users, :encrypted_password, true + change_column_null :users, :password_salt, true + end +end diff --git a/db/openneo_id_schema.rb b/db/openneo_id_schema.rb index 4a630c3b..8df274bc 100644 --- a/db/openneo_id_schema.rb +++ b/db/openneo_id_schema.rb @@ -10,12 +10,12 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_13_200849) do +ActiveRecord::Schema[7.1].define(version: 2024_03_15_020053) do create_table "users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t| t.string "name", limit: 20, null: false - t.string "encrypted_password", limit: 64, null: false - t.string "email", limit: 50, null: false - t.string "password_salt", limit: 32, null: false + t.string "encrypted_password", limit: 64 + t.string "email", limit: 50 + t.string "password_salt", limit: 32 t.string "reset_password_token" t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at", precision: nil