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.
This commit is contained in:
Emi Matchu 2024-03-14 19:11:06 -07:00
parent 31a11a04fa
commit 3eeb5d1065
5 changed files with 116 additions and 10 deletions

View file

@ -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

View file

@ -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

View file

@ -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}`);
}

View file

@ -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

View file

@ -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