Skip to content

Commit

Permalink
feat(core): Export ExternalAuthenticationService
Browse files Browse the repository at this point in the history
Used to simplify the creation of external authentication strategies
  • Loading branch information
michaelbromley committed Jun 24, 2020
1 parent 39c743b commit c3ed2cd
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 93 deletions.
54 changes: 10 additions & 44 deletions packages/core/e2e/fixtures/test-authentication-strategies.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import {
AuthenticationStrategy,
Customer,
ExternalAuthenticationMethod,
ExternalAuthenticationService,
Injector,
RequestContext,
RoleService,
User,
} from '@vendure/core';
import { DocumentNode } from 'graphql';
import gql from 'graphql-tag';
import { Connection } from 'typeorm';

export const VALID_AUTH_TOKEN = 'valid-auth-token';

Expand All @@ -24,12 +21,10 @@ export type TestAuthPayload = {

export class TestAuthenticationStrategy implements AuthenticationStrategy<TestAuthPayload> {
readonly name = 'test_strategy';
private connection: Connection;
private roleService: RoleService;
private externalAuthenticationService: ExternalAuthenticationService;

init(injector: Injector) {
this.connection = injector.getConnection();
this.roleService = injector.get(RoleService);
this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
}

defineInputType(): DocumentNode {
Expand All @@ -52,47 +47,18 @@ export class TestAuthenticationStrategy implements AuthenticationStrategy<TestAu
if (data.token !== VALID_AUTH_TOKEN) {
return false;
}
const user = await this.connection
.getRepository(User)
.createQueryBuilder('user')
.leftJoinAndSelect('user.authenticationMethods', 'authMethod')
.where('authMethod.externalIdentifier = :token', { token: data.token })
.getOne();
const user = await this.externalAuthenticationService.findUser(this.name, data.token);

if (user) {
return user;
}
return this.createNewCustomerAndUser(data);
}

private async createNewCustomerAndUser(data: TestAuthPayload) {
const { token, userData } = data;
const customerRole = await this.roleService.getCustomerRole();
const newUser = new User({
identifier: data.userData.email,
roles: [customerRole],
return this.externalAuthenticationService.createCustomerAndUser(ctx, {
strategy: this.name,
externalIdentifier: data.token,
emailAddress: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
verified: true,
});

const authMethod = await this.connection.manager.save(
new ExternalAuthenticationMethod({
externalIdentifier: data.token,
provider: this.name,
}),
);

newUser.authenticationMethods = [authMethod];
const savedUser = await this.connection.manager.save(newUser);

const customer = await this.connection.manager.save(
new Customer({
emailAddress: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
user: savedUser,
}),
);

return savedUser;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class ExternalAuthenticationMethod extends AuthenticationMethod {
}

@Column()
provider: string;
strategy: string;

@Column()
externalIdentifier: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/typeorm';
import { HistoryEntryType } from '@vendure/common/lib/generated-types';
import { Connection } from 'typeorm';

import { RequestContext } from '../../../api/common/request-context';
import { ExternalAuthenticationMethod } from '../../../entity/authentication-method/external-authentication-method.entity';
import { Customer } from '../../../entity/customer/customer.entity';
import { User } from '../../../entity/user/user.entity';
import { HistoryService } from '../../services/history.service';
import { RoleService } from '../../services/role.service';

/**
* @description
* This is a helper service which exposes methods related to looking up and creating Users based on an
* external {@link AuthenticationStrategy}.
*
* @docsCategory auth
*/
@Injectable()
export class ExternalAuthenticationService {
constructor(
@InjectConnection() private connection: Connection,
private roleService: RoleService,
private historyService: HistoryService,
) {}

/**
* @description
* Looks up a User based on their identifier from an external authentication
* provider.
*/
async findUser(strategy: string, externalIdentifier: string): Promise<User | undefined> {
return await this.connection
.getRepository(User)
.createQueryBuilder('user')
.leftJoinAndSelect('user.authenticationMethods', 'authMethod')
.where('authMethod.strategy = :strategy', { strategy })
.andWhere('authMethod.externalIdentifier = :externalIdentifier', { externalIdentifier })
.andWhere('user.deletedAt IS NULL')
.getOne();
}

/**
* @description
* If a user has been successfully authenticated by an external authentication provider, yet cannot
* be found using `findUserByExternalAuthenticationMethod`, then we need to create a new User and
* Customer record in Vendure for that user. This method encapsulates that logic as well as additional
* housekeeping such as adding a record to the Customer's history.
*/
async createCustomerAndUser(
ctx: RequestContext,
config: {
strategy: string;
externalIdentifier: string;
verified: boolean;
emailAddress: string;
firstName?: string;
lastName?: string;
},
): Promise<User> {
const customerRole = await this.roleService.getCustomerRole();
const newUser = new User({
identifier: config.emailAddress,
roles: [customerRole],
verified: config.verified || false,
});

const authMethod = await this.connection.manager.save(
new ExternalAuthenticationMethod({
externalIdentifier: config.externalIdentifier,
strategy: config.strategy,
}),
);

newUser.authenticationMethods = [authMethod];
const savedUser = await this.connection.manager.save(newUser);

const customer = await this.connection.manager.save(
new Customer({
emailAddress: config.emailAddress,
firstName: config.firstName,
lastName: config.lastName,
user: savedUser,
}),
);

await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: HistoryEntryType.CUSTOMER_REGISTERED,
data: {
strategy: config.strategy,
},
});

if (config.verified) {
await this.historyService.createHistoryEntryForCustomer({
customerId: customer.id,
ctx,
type: HistoryEntryType.CUSTOMER_VERIFIED,
data: {
strategy: config.strategy,
},
});
}

return savedUser;
}
}
1 change: 1 addition & 0 deletions packages/core/src/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './helpers/utils/patch-entity';
export * from './helpers/utils/channel-aware-orm-utils';
export * from './helpers/utils/get-entity-or-throw';
export * from './helpers/list-query-builder/list-query-builder';
export * from './helpers/external-authentication/external-authentication.service';
export * from './helpers/order-calculator/order-calculator';
export * from './helpers/order-state-machine/order-state';
export * from './helpers/payment-state-machine/payment-state';
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/service/service.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { WorkerServiceModule } from '../worker/worker-service.module';

import { CollectionController } from './controllers/collection.controller';
import { TaxRateController } from './controllers/tax-rate.controller';
import { ExternalAuthenticationService } from './helpers/external-authentication/external-authentication.service';
import { ListQueryBuilder } from './helpers/list-query-builder/list-query-builder';
import { OrderCalculator } from './helpers/order-calculator/order-calculator';
import { OrderMerger } from './helpers/order-merger/order-merger';
Expand Down Expand Up @@ -98,6 +99,7 @@ const helpers = [
RefundStateMachine,
ShippingConfiguration,
SlugValidator,
ExternalAuthenticationService,
];

const workerControllers = [CollectionController, TaxRateController];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import {
AuthenticationStrategy,
Customer,
ExternalAuthenticationMethod,
ExternalAuthenticationService,
Injector,
RequestContext,
RoleService,
User,
} from '@vendure/core';
import { OAuth2Client } from 'google-auth-library';
import { TokenPayload } from 'google-auth-library/build/src/auth/loginticket';
import { DocumentNode } from 'graphql';
import gql from 'graphql-tag';
import { Connection } from 'typeorm';

export type GoogleAuthData = {
token: string;
Expand All @@ -20,16 +16,14 @@ export type GoogleAuthData = {
export class GoogleAuthenticationStrategy implements AuthenticationStrategy<GoogleAuthData> {
readonly name = 'google';
private client: OAuth2Client;
private connection: Connection;
private roleService: RoleService;
private externalAuthenticationService: ExternalAuthenticationService;

constructor(private clientId: string) {
this.client = new OAuth2Client(clientId);
}

init(injector: Injector) {
this.connection = injector.getConnection();
this.roleService = injector.get(RoleService);
this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
}

defineInputType(): DocumentNode {
Expand All @@ -49,50 +43,20 @@ export class GoogleAuthenticationStrategy implements AuthenticationStrategy<Goog
audience: this.clientId,
});
const payload = ticket.getPayload();
if (!payload) {
if (!payload || !payload.email) {
return false;
}

const user = await this.connection
.getRepository(User)
.createQueryBuilder('user')
.leftJoinAndSelect('user.authenticationMethods', 'authMethod')
.where('authMethod.externalIdentifier = :sub', { sub: payload.sub })
.getOne();

const user = await this.externalAuthenticationService.findUser(this.name, payload.sub);
if (user) {
return user;
}
return this.createNewCustomerAndUser(payload);
}

private async createNewCustomerAndUser(data: TokenPayload) {
const customerRole = await this.roleService.getCustomerRole();
const newUser = new User({
identifier: data.email,
roles: [customerRole],
verified: data.email_verified || false,
return this.externalAuthenticationService.createCustomerAndUser(ctx, {
strategy: this.name,
externalIdentifier: payload.sub,
verified: payload.email_verified || false,
emailAddress: payload.email,
firstName: payload.given_name,
lastName: payload.family_name,
});

const authMethod = await this.connection.manager.save(
new ExternalAuthenticationMethod({
externalIdentifier: data.sub,
provider: this.name,
}),
);

newUser.authenticationMethods = [authMethod];
const savedUser = await this.connection.manager.save(newUser);

const customer = await this.connection.manager.save(
new Customer({
emailAddress: data.email,
firstName: data.given_name,
lastName: data.family_name,
user: savedUser,
}),
);

return savedUser;
}
}

0 comments on commit c3ed2cd

Please sign in to comment.