diff --git a/packages/auth/src/db/constants.js b/packages/auth/src/db/constants.js index 477968975a..ecdaae5bad 100644 --- a/packages/auth/src/db/constants.js +++ b/packages/auth/src/db/constants.js @@ -13,6 +13,7 @@ exports.DocumentTypes = { APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`, APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`, ROLE: "role", + MIGRATIONS: "migrations", } exports.StaticDatabases = { diff --git a/packages/auth/src/db/views.js b/packages/auth/src/db/views.js index 1b48786e24..fd004ca0c2 100644 --- a/packages/auth/src/db/views.js +++ b/packages/auth/src/db/views.js @@ -21,7 +21,7 @@ exports.createUserEmailView = async db => { // if using variables in a map function need to inject them before use map: `function(doc) { if (doc._id.startsWith("${DocumentTypes.USER}")) { - emit(doc.email, doc._id) + emit(doc.email.toLowerCase(), doc._id) } }`, } diff --git a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js index e2ad9a9300..81b8c3e6b0 100644 --- a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js +++ b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js @@ -72,7 +72,6 @@ describe("third party common", () => { const expectUserIsSynced = (user, thirdPartyUser) => { expect(user.provider).toBe(thirdPartyUser.provider) - expect(user.email).toBe(thirdPartyUser.email) expect(user.firstName).toBe(thirdPartyUser.profile.name.givenName) expect(user.lastName).toBe(thirdPartyUser.profile.name.familyName) expect(user.thirdPartyProfile).toStrictEqual(thirdPartyUser.profile._json) @@ -135,6 +134,24 @@ describe("third party common", () => { }) }) + describe("exists by email with different casing", () => { + beforeEach(async () => { + id = generateGlobalUserID(newid()) // random id + email = thirdPartyUser.email.toUpperCase() // matching email except for casing + await createUser() + }) + + it("syncs and authenticates the user", async () => { + await authenticateThirdParty(thirdPartyUser, true, done, saveUser) + + const user = expectUserIsAuthenticated() + expectUserIsSynced(user, thirdPartyUser) + expectUserIsUpdated(user) + expect(user.email).toBe(thirdPartyUser.email.toUpperCase()) + }) + }) + + describe("exists by id", () => { beforeEach(async () => { id = generateGlobalUserID(thirdPartyUser.userId) // matching id diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index 54a5504712..b467c0b10b 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -66,12 +66,16 @@ exports.authenticateThirdParty = async function ( // setup a blank user using the third party id dbUser = { _id: userId, + email: thirdPartyUser.email, roles: {}, } } dbUser = await syncUser(dbUser, thirdPartyUser) + // never prompt for password reset + dbUser.forceResetPassword = false + // create or sync the user let response try { @@ -122,9 +126,6 @@ async function syncUser(user, thirdPartyUser) { user.provider = thirdPartyUser.provider user.providerType = thirdPartyUser.providerType - // email - user.email = thirdPartyUser.email - if (thirdPartyUser.profile) { const profile = thirdPartyUser.profile diff --git a/packages/auth/src/migrations/index.js b/packages/auth/src/migrations/index.js new file mode 100644 index 0000000000..ca06188a8a --- /dev/null +++ b/packages/auth/src/migrations/index.js @@ -0,0 +1,65 @@ +const { DocumentTypes } = require("../db/constants") +const { getGlobalDB } = require("../tenancy") + +exports.MIGRATION_DBS = { + GLOBAL_DB: "GLOBAL_DB", +} + +exports.MIGRATIONS = { + USER_EMAIL_VIEW_CASING: "user_email_view_casing", +} + +const DB_LOOKUP = { + [exports.MIGRATION_DBS.GLOBAL_DB]: [ + exports.MIGRATIONS.USER_EMAIL_VIEW_CASING, + ], +} + +const getMigrationsDoc = async db => { + // get the migrations doc + try { + return await db.get(DocumentTypes.MIGRATIONS) + } catch (err) { + if (err.status && err.status === 404) { + return { _id: DocumentTypes.MIGRATIONS } + } + } +} + +exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => { + let db + if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) { + db = getGlobalDB() + } else { + throw new Error(`Unrecognised migration db [${migrationDb}]`) + } + + if (!DB_LOOKUP[migrationDb].includes(migrationName)) { + throw new Error( + `Unrecognised migration name [${migrationName}] for db [${migrationDb}]` + ) + } + return tryMigrate(db, migrationName, migrateFn) +} + +const tryMigrate = async (db, migrationName, migrateFn) => { + try { + const doc = await getMigrationsDoc(db) + + // exit if the migration has been performed + if (doc[migrationName]) { + return + } + + console.log(`Performing migration: ${migrationName}`) + await migrateFn() + console.log(`Migration complete: ${migrationName}`) + + // mark as complete + doc[migrationName] = Date.now() + await db.put(doc) + } catch (err) { + console.error(`Error performing migration: ${migrationName}: `, err) + throw err + } +} diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index 823fd06322..e1df289d6e 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -20,6 +20,9 @@ const { hash } = require("./hashing") const userCache = require("./cache/user") const env = require("./environment") const { getUserSessions, invalidateSessions } = require("./security/sessions") +const { migrateIfRequired } = require("./migrations") +const { USER_EMAIL_VIEW_CASING } = require("./migrations").MIGRATIONS +const { GLOBAL_DB } = require("./migrations").MIGRATION_DBS const APP_PREFIX = DocumentTypes.APP + SEPARATOR @@ -128,10 +131,16 @@ exports.getGlobalUserByEmail = async email => { throw "Must supply an email address to view" } const db = getGlobalDB() + + await migrateIfRequired(GLOBAL_DB, USER_EMAIL_VIEW_CASING, async () => { + // re-create the view with latest changes + await createUserEmailView(db) + }) + try { let users = ( await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { - key: email, + key: email.toLowerCase(), include_docs: true, }) ).rows