Skip to content

Commit

Permalink
feat(core): Normalize email addresses for native auth
Browse files Browse the repository at this point in the history
Fixes #1515
  • Loading branch information
michaelbromley committed Mar 16, 2023
1 parent 9b4821e commit ad7eab8
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 8 deletions.
2 changes: 1 addition & 1 deletion packages/core/e2e/graphql/generated-e2e-admin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8286,7 +8286,7 @@ export type GetCustomerListQuery = {
lastName: string;
emailAddress: string;
phoneNumber?: string | null;
user?: { id: string; verified: boolean } | null;
user?: { id: string; identifier: string; verified: boolean } | null;
}>;
};
};
Expand Down
1 change: 1 addition & 0 deletions packages/core/e2e/graphql/shared-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const GET_CUSTOMER_LIST = gql`
phoneNumber
user {
id
identifier
verified
}
}
Expand Down
78 changes: 77 additions & 1 deletion packages/core/e2e/shop-auth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1050,7 +1050,7 @@ describe('Expiring tokens', () => {
});

describe('Registration without email verification', () => {
const { server, shopClient } = createTestEnvironment(
const { server, shopClient, adminClient } = createTestEnvironment(
mergeConfig(testConfig(), {
plugins: [TestEmailPlugin as any],
authOptions: {
Expand All @@ -1066,6 +1066,7 @@ describe('Registration without email verification', () => {
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
customerCount: 1,
});
await adminClient.asSuperAdmin();
}, TEST_SETUP_TIMEOUT_MS);

beforeEach(() => {
Expand Down Expand Up @@ -1127,6 +1128,81 @@ describe('Registration without email verification', () => {
);
expect(result.me.identifier).toBe(userEmailAddress);
});

it('can login case insensitive', async () => {
await shopClient.asUserWithCredentials(userEmailAddress.toUpperCase(), 'test');

const result = await shopClient.query(
gql`
query GetMe {
me {
identifier
}
}
`,
);
expect(result.me.identifier).toBe(userEmailAddress);
});

it('normalizes customer & user email addresses', async () => {
const input: RegisterCustomerInput = {
firstName: 'Bobbington',
lastName: 'Jarrolds',
emailAddress: 'BOBBINGTON.J@Test.com',
password: 'test',
};
const { registerCustomerAccount } = await shopClient.query<
CodegenShop.RegisterMutation,
CodegenShop.RegisterMutationVariables
>(REGISTER_ACCOUNT, {
input,
});
successErrorGuard.assertSuccess(registerCustomerAccount);

const { customers } = await adminClient.query<
Codegen.GetCustomerListQuery,
Codegen.GetCustomerListQueryVariables
>(GET_CUSTOMER_LIST, {
options: {
filter: {
firstName: { eq: 'Bobbington' },
},
},
});

expect(customers.items[0].emailAddress).toBe('bobbington.j@test.com');
expect(customers.items[0].user?.identifier).toBe('bobbington.j@test.com');
});

it('registering with same email address with different casing does not create new user', async () => {
const input: RegisterCustomerInput = {
firstName: 'Glen',
lastName: 'Beardsley',
emailAddress: userEmailAddress.toUpperCase(),
password: 'test',
};
const { registerCustomerAccount } = await shopClient.query<
CodegenShop.RegisterMutation,
CodegenShop.RegisterMutationVariables
>(REGISTER_ACCOUNT, {
input,
});
successErrorGuard.assertSuccess(registerCustomerAccount);

const { customers } = await adminClient.query<
Codegen.GetCustomerListQuery,
Codegen.GetCustomerListQueryVariables
>(GET_CUSTOMER_LIST, {
options: {
filter: {
firstName: { eq: 'Glen' },
},
},
});

expect(customers.items[0].emailAddress).toBe(userEmailAddress);
expect(customers.items[0].user?.identifier).toBe(userEmailAddress);
});
});

describe('Updating email address without email verification', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/service/services/administrator.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { In, IsNull } from 'typeorm';
import { RequestContext } from '../../api/common/request-context';
import { RelationPaths } from '../../api/index';
import { EntityNotFoundError, InternalServerError, UserInputError } from '../../common/error/errors';
import { idsAreEqual } from '../../common/index';
import { idsAreEqual, normalizeEmailAddress } from '../../common/index';
import { ListQueryOptions } from '../../common/types/common-types';
import { ConfigService } from '../../config';
import { TransactionalConnection } from '../../connection/transactional-connection';
Expand Down Expand Up @@ -127,6 +127,7 @@ export class AdministratorService {
async create(ctx: RequestContext, input: CreateAdministratorInput): Promise<Administrator> {
await this.checkActiveUserCanGrantRoles(ctx, input.roleIds);
const administrator = new Administrator(input);
administrator.emailAddress = normalizeEmailAddress(input.emailAddress);
administrator.user = await this.userService.createAdminUser(ctx, input.emailAddress, input.password);
let createdAdministrator = await this.connection
.getRepository(ctx, Administrator)
Expand Down
13 changes: 8 additions & 5 deletions packages/core/src/service/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
VerificationTokenExpiredError,
VerificationTokenInvalidError,
} from '../../common/error/generated-graphql-shop-errors';
import { normalizeEmailAddress } from '../../common/index';
import { ConfigService } from '../../config/config.service';
import { TransactionalConnection } from '../../connection/transactional-connection';
import { NativeAuthenticationMethod } from '../../entity/authentication-method/native-authentication-method.entity';
Expand Down Expand Up @@ -72,7 +73,9 @@ export class UserService {
.leftJoinAndSelect('user.roles', 'roles')
.leftJoinAndSelect('roles.channels', 'channels')
.leftJoinAndSelect('user.authenticationMethods', 'authenticationMethods')
.where('user.identifier = :identifier', { identifier: emailAddress })
.where('LOWER(user.identifier) = :identifier', {
identifier: normalizeEmailAddress(emailAddress),
})
.andWhere('user.deletedAt IS NULL')
.getOne()
.then(result => result ?? undefined);
Expand All @@ -88,7 +91,7 @@ export class UserService {
password?: string,
): Promise<User | PasswordValidationError> {
const user = new User();
user.identifier = identifier;
user.identifier = normalizeEmailAddress(identifier);
const customerRole = await this.roleService.getCustomerRole(ctx);
user.roles = [customerRole];
const addNativeAuthResult = await this.addNativeAuthenticationMethod(ctx, user, identifier, password);
Expand Down Expand Up @@ -138,7 +141,7 @@ export class UserService {
} else {
authenticationMethod.passwordHash = '';
}
authenticationMethod.identifier = identifier;
authenticationMethod.identifier = normalizeEmailAddress(identifier);
authenticationMethod.user = user;
await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(authenticationMethod);
user.authenticationMethods = [...(user.authenticationMethods ?? []), authenticationMethod];
Expand All @@ -151,14 +154,14 @@ export class UserService {
*/
async createAdminUser(ctx: RequestContext, identifier: string, password: string): Promise<User> {
const user = new User({
identifier,
identifier: normalizeEmailAddress(identifier),
verified: true,
});
const authenticationMethod = await this.connection
.getRepository(ctx, NativeAuthenticationMethod)
.save(
new NativeAuthenticationMethod({
identifier,
identifier: normalizeEmailAddress(identifier),
passwordHash: await this.passwordCipher.hash(password),
}),
);
Expand Down

0 comments on commit ad7eab8

Please sign in to comment.