Explorar el Código

feat(core): Export ExternalAuthenticationService

Used to simplify the creation of external authentication strategies
Michael Bromley hace 5 años
padre
commit
c3ed2cd2ce

+ 10 - 44
packages/core/e2e/fixtures/test-authentication-strategies.ts

@@ -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';
 
@@ -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 {
@@ -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;
     }
 }

+ 1 - 1
packages/core/src/entity/authentication-method/external-authentication-method.entity.ts

@@ -10,7 +10,7 @@ export class ExternalAuthenticationMethod extends AuthenticationMethod {
     }
 
     @Column()
-    provider: string;
+    strategy: string;
 
     @Column()
     externalIdentifier: string;

+ 110 - 0
packages/core/src/service/helpers/external-authentication/external-authentication.service.ts

@@ -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 - 0
packages/core/src/service/index.ts

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

+ 2 - 0
packages/core/src/service/service.module.ts

@@ -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';
@@ -98,6 +99,7 @@ const helpers = [
     RefundStateMachine,
     ShippingConfiguration,
     SlugValidator,
+    ExternalAuthenticationService,
 ];
 
 const workerControllers = [CollectionController, TaxRateController];

+ 12 - 48
packages/dev-server/test-plugins/google-auth/google-authentication-strategy.ts

@@ -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;
@@ -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 {
@@ -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;
     }
 }