Skip to content

Commit 84d1f5c

Browse files
fix: apply lower case to all email invitations (#8121)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 63a9126 commit 84d1f5c

5 files changed

Lines changed: 67 additions & 15 deletions

File tree

.changeset/real-dingos-punch.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'hive': patch
3+
---
4+
5+
Fix issue where the user emails were not inserted in lower-case for OIDC providers returning non-lowercase emails.
6+
7+
If you are affected, you can manually fix you database state by running the following commands, to make account-linking from different login methods work smoothly.
8+
9+
```sql
10+
UPDATE "users"
11+
SET
12+
"email" = lower("email")
13+
WHERE
14+
"email" <> lower("email")
15+
;
16+
```
17+
18+
19+
```sql
20+
UPDATE "supertokens_thirdparty_users"
21+
SET
22+
"email" = lower("email")
23+
WHERE
24+
"email" <> lower("email")
25+
;
26+
```
27+
28+
29+
Fix issue where user emails were not inserted into the database in lowercase for invites, resulting in a mismatch of user account email and invite email that could not be accespted.
30+
To cleanup your database of invites, run the following command to identify duplicate records and then manually fix them/clean them up.
31+
32+
```sql
33+
SELECT
34+
"organization_id"
35+
, lower(email) AS key
36+
, array_agg(json_object(array['email', 'code', 'expires_at'], array["email", "code", to_json("expires_at")::text])) AS records
37+
, COUNT(*)
38+
FROM
39+
"organization_invitations"
40+
GROUP BY
41+
"organization_id", lower("email")
42+
HAVING COUNT(*) > 1;
43+
```

packages/services/api/src/modules/auth/providers/email-verification.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class EmailVerification {
8484
FROM "email_verifications" "ev"
8585
WHERE
8686
"ev"."user_identity_id" = ${input.userIdentityId}
87-
AND "ev"."email" = ${input.email}
87+
AND lower("ev"."email") = lower(${input.email})
8888
`,
8989
)
9090
.then(v => EmailVerificationModel.nullable().parse(v));
@@ -143,7 +143,7 @@ export class EmailVerification {
143143
FROM "email_verifications" "ev"
144144
WHERE
145145
"ev"."user_identity_id" = ${input.userIdentityId}
146-
AND "ev"."email" = ${superTokensUser.email}
146+
AND lower("ev"."email") = lower(${superTokensUser.email})
147147
`,
148148
)
149149
.then(v => EmailVerificationModel.nullable().parse(v));
@@ -216,7 +216,7 @@ export class EmailVerification {
216216
FROM "email_verifications" "ev"
217217
WHERE
218218
"user_identity_id" = ${input.userIdentityId}
219-
AND "email" = ${input.email}
219+
AND lower("email") = lower(${input.email})
220220
AND "expires_at" IS NOT NULL
221221
AND "verified_at" IS NULL
222222
`,

packages/services/api/src/modules/auth/providers/supertokens-store.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ export class SuperTokensStore {
329329
UPDATE
330330
"supertokens_thirdparty_users"
331331
SET
332-
"email" = ${args.newEmail}
332+
"email" = lower(${args.newEmail})
333333
WHERE
334334
"app_id" = 'public'
335335
AND "user_id" = ${args.userId}
@@ -387,7 +387,7 @@ export class SuperTokensStore {
387387
, ${args.thirdPartyId}
388388
, ${args.thirdPartyUserId}
389389
, ${userId}
390-
, ${args.email}
390+
, lower(${args.email})
391391
, ${now}
392392
)
393393
RETURNING
@@ -560,7 +560,7 @@ export class SuperTokensStore {
560560
'public'
561561
, ${args.user.userId}
562562
, ${args.token}
563-
, ${args.user.email}
563+
, lower(${args.user.email})
564564
, ${args.expiresAt}
565565
)
566566
RETURNING

packages/services/server/src/supertokens-at-home.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1411,7 +1411,13 @@ export async function registerSupertokensAtHome(
14111411
const userInfoBody = z
14121412
.object({
14131413
sub: z.string(),
1414-
email: z.string().optional().nullable(),
1414+
email: z
1415+
.string()
1416+
// make sure the email is transformed to lower-case
1417+
// sometimes the OIDC provider love giving us custom formatted ones
1418+
.transform(email => email.toLowerCase())
1419+
.optional()
1420+
.nullable(),
14151421
})
14161422
.safeParse(userInfoBodyJSON.data);
14171423

packages/services/storage/src/index.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,10 @@ export async function createStorage(
282282
},
283283
async ensureUserExists({
284284
superTokensUserId,
285-
email,
286285
oidcIntegration,
287286
firstName,
288287
lastName,
288+
...args
289289
}: {
290290
superTokensUserId: string;
291291
firstName: string | null;
@@ -295,6 +295,7 @@ export async function createStorage(
295295
id: string;
296296
};
297297
}) {
298+
const email = args.email.toLowerCase();
298299
class EnsureUserExistsError extends Error {}
299300

300301
try {
@@ -326,7 +327,7 @@ export async function createStorage(
326327
psql`/* ensureUserExists */
327328
SELECT ${userFields(psql`"users".`)}
328329
FROM "users"
329-
WHERE "users"."email" = ${email}
330+
WHERE "users"."email" = lower(${email})
330331
ORDER BY "users"."created_at";
331332
`,
332333
)
@@ -358,7 +359,7 @@ export async function createStorage(
358359
DELETE FROM "organization_invitations" AS "oi"
359360
WHERE
360361
"oi"."organization_id" = ${oidcConfig.linkedOrganizationId}
361-
AND "oi"."email" = ${email}
362+
AND lower("oi"."email") = lower(${email})
362363
AND "oi"."expires_at" > now()
363364
RETURNING
364365
"oi"."organization_id" "organizationId"
@@ -427,10 +428,10 @@ export async function createStorage(
427428
if (internalUser.email !== email) {
428429
await t.query(psql`
429430
UPDATE "users"
430-
SET "email" = ${email}
431+
SET "email" = lower(${email})
431432
WHERE "id" = ${internalUser.id}
432433
`);
433-
internalUser.email = email;
434+
internalUser.email = email.toLowerCase();
434435
}
435436

436437
if (oidcIntegration !== null) {
@@ -958,7 +959,7 @@ export async function createStorage(
958959
)
959960
VALUES (
960961
${args.organizationId}
961-
, ${args.email}
962+
, lower(${args.email})
962963
, ${args.roleId}
963964
, ${args.resourceAssignments === null ? null : psql.jsonb(args.resourceAssignments)}
964965
)
@@ -983,7 +984,9 @@ export async function createStorage(
983984
.maybeOne(
984985
psql`/* deleteOrganizationInvitationByEmail */
985986
DELETE FROM organization_invitations
986-
WHERE organization_id = ${organization} AND email = ${email}
987+
WHERE
988+
organization_id = ${organization}
989+
AND email = lower(${email})
987990
RETURNING
988991
"organization_id" AS "organizationId"
989992
, "code"
@@ -1210,7 +1213,7 @@ export async function createStorage(
12101213
LEFT JOIN organization_invitations as i ON (i.organization_id = o.id)
12111214
WHERE
12121215
i.code = ${inviteCode}
1213-
AND i.email = ${email}
1216+
AND i.email = lower(${email})
12141217
AND i.expires_at > NOW()
12151218
GROUP BY o.id
12161219
LIMIT 1

0 commit comments

Comments
 (0)