Просмотр исходного кода

feat(docs): Create authentication guide

Michael Bromley 5 лет назад
Родитель
Сommit
ea0bb56ca7

+ 281 - 0
docs/content/docs/developer-guide/authentication.md

@@ -0,0 +1,281 @@
+---
+title: 'Authentication'
+showtoc: true
+---
+
+# Authentication
+
+Authentication is the process of determining the identity of a user. Common ways of authenticating a user are by asking the user for secret credentials (username & password) or by a third-party authentication provider such as Facebook or Google login.
+
+By default, Vendure uses a username/email address and password to authenticate users, but also supports a wide range of authentication methods via configurable AuthenticationStrategies.
+
+## Adding support for external authentication
+
+This is done via the [`VendureConfig.authOptions` object]({{< relref "auth-options" >}}#shopauthenticationstrategy):
+
+```TypeScript
+export const config: VendureConfig = {
+  authOptions: {
+      shopAuthenticationStrategy: [
+        new NativeAuthenticationStrategy(),
+        new FacebookAuthenticationStrategy(),
+        new GoogleAuthenticationStrategy(),
+      ],
+      adminAuthenticationStrategy: [
+        new NativeAuthenticationStrategy(),
+        new KeycloakAuthenticationStrategy(),
+      ],
+  }
+}
+```
+
+In the above example, we define the strategies available for authenticating in the Shop API and the Admin API. The `NativeAuthenticationStrategy` is the only one actually provided by Vendure out-of-the-box, and this is the default username/email + password strategy.
+
+The other strategies would be custom-built (or provided by future npm packages) but creating classes that implement the [`AuthenticationStrategy` interface]({{< relref "authentication-strategy" >}}).
+
+Let's take a look at a couple of examples of what a customer AuthenticationStrategy implementation would look like.
+
+## Example: Google authentication
+
+This example demonstrates how to implement a Google login flow.
+
+### Storefront setup
+
+In your storefront, you need to integrate the Google sign-in button as described in ["Integrating Google Sign-In into your web app"](https://developers.google.com/identity/sign-in/web/sign-in). Successful authentication will result in a `onSignIn` function being called in your app. It will look something like this:
+
+```TypeScript
+function onSignIn(googleUser) {
+  graphQlQuery(
+    `mutation Authenticate($token: String!) {
+        authenticate(input: {
+          google: { token: $token }
+        }) {
+        user {
+            id
+            identifier
+        }
+      }
+    }`,
+    { token: googleUser.getAuthResponse().id_token }
+  ).then(() => {
+    // redirect to account page
+  });
+}
+```
+
+### Backend
+
+On the backend, you'll need to define an AuthenticationStrategy to take the authorization token provided by the
+storefront in the `authenticate` mutation, and use it to get the necessary personal information on that user from
+Google.
+
+To do this you'll need to install the `google-auth-library` npm package as described in the ["Authenticate with a backend server" guide](https://developers.google.com/identity/sign-in/web/backend-auth).
+
+```TypeScript
+{
+ AuthenticationStrategy,
+  ExternalAuthenticationService,
+  Injector,
+  RequestContext,
+  User,
+} from '@vendure/core';
+import { OAuth2Client } from 'google-auth-library';
+import { DocumentNode } from 'graphql';
+import gql from 'graphql-tag';
+
+export type GoogleAuthData = {
+  token: string;
+};
+
+export class GoogleAuthenticationStrategy implements AuthenticationStrategy<GoogleAuthData> {
+  readonly name = 'google';
+  private client: OAuth2Client;
+  private externalAuthenticationService: ExternalAuthenticationService;
+
+  constructor(private clientId: string) {
+    // The clientId is obtained by creating a new OAuth client ID as described
+    // in the Google guide linked above.
+    this.client = new OAuth2Client(clientId);
+  }
+
+  init(injector: Injector) {
+    // The ExternalAuthenticationService is a helper service which encapsulates much
+    // of the common functionality related to dealing with external authentication
+    // providers.
+    this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
+  }
+
+  defineInputType(): DocumentNode {
+    // Here we define the expected input object expected by the `authenticate` mutation
+    // under the "google" key.
+    return gql`
+        input GoogleAuthInput {
+            token: String!
+        }
+    `;
+  }
+
+  async authenticate(ctx: RequestContext, data: GoogleAuthData): Promise<User | false> {
+    // Here is the logic that uses the token provided by the storefront and uses it
+    // to find the user data from Google.
+    const ticket = await this.client.verifyIdToken({
+        idToken: data.token,
+        audience: this.clientId,
+    });
+    const payload = ticket.getPayload();
+    if (!payload || !payload.email) {
+        return false;
+    }
+
+    // First we check to see if this user has already authenticated in our
+    // Vendure server using this Google account. If so, we return that
+    // User object, and they will be now authenticated in Vendure.
+    const user = await this.externalAuthenticationService.findCustomerUser(this.name, payload.sub);
+    if (user) {
+        return user;
+    }
+
+    // If no user was found, we need to create a new User and Customer based
+    // on the details provided by Google. The ExternalAuthenticationService
+    // provides a convenience method which encapsulates all of this into
+    // a single method call.
+    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,
+    });
+  }
+}
+```
+
+## Example: Keycloak authentication
+
+Here's an example of an AuthenticationStrategy intended to be used on the Admin API. The use-case is when the company has an existing identity server for employees, and you'd like your Vendure shop admins to be able to authenticate with their existing accounts.
+
+This example uses [Keycloak](https://www.keycloak.org/), a popular open-source identity management server. To get your own Keycloak server up and running in minutes, follow the [Keycloak on Docker](https://www.keycloak.org/getting-started/getting-started-docker) guide.
+
+### Configure a login page & Admin UI
+
+In this example we'll assume the login page is hosted at `http://intranet/login`. We'll also assume that a "login to Vendure" button has been added to that pagem and that the page is using the [Keycloak JavaScript adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter), which can be used to get the current user's authorization token:
+
+```JavaScript
+vendureLoginButton.addEventListener('click', () => {
+  return graphQlQuery(`
+    mutation Authenticate($token: String!) {
+      authenticate(input: {
+        keycloak: {
+          token: $token
+        }
+      }) {
+        user { id }
+      }
+    }`,
+    { token: keycloak.token },
+  )
+  .then((result) => {
+      if (result.data?.authenticate.user) {
+          // successfully authenticated - redirect to Vendure Admin UI
+          window.location.replace('http://localhost:3000/admin');
+      }
+  });
+});
+```
+
+We also need to tell the Admin UI application about the custom login URL, since we have no need for the default "username/password" login form. This can be done by setting the [`loginUrl` property]({{< relref "admin-ui-config" >}}#loginurl) in the AdminUiConfig:
+
+```TypeScript
+// vendure-config.ts
+plugins: [
+  AdminUiPlugin.init({
+    port: 5001,
+    adminUiConfig: {
+      loginUrl: 'http://intranet/login',
+    },
+  }),
+]
+```
+
+### Backend
+
+The backend part is very similar to the Google authentication example (they both use the OpenID Connect standard), so we'll not duplicate the explanatory comments here:
+
+```TypeScript
+import { HttpService } from '@nestjs/common';
+import {
+    AuthenticationStrategy,
+    ExternalAuthenticationService,
+    Injector,
+    Logger,
+    RequestContext,
+    RoleService,
+    User,
+} from '@vendure/core';
+import { DocumentNode } from 'graphql';
+import gql from 'graphql-tag';
+
+export type KeycloakAuthData = {
+    token: string;
+};
+
+export class KeycloakAuthenticationStrategy implements AuthenticationStrategy<KeycloakAuthData> {
+  readonly name = 'keycloak';
+  private externalAuthenticationService: ExternalAuthenticationService;
+  private httpService: HttpService;
+  private roleService: RoleService;
+
+  init(injector: Injector) {
+    this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
+    this.httpService = injector.get(HttpService);
+    this.roleService = injector.get(RoleService);
+  }
+
+  defineInputType(): DocumentNode {
+    return gql`
+      input KeycloakAuthInput {
+        token: String!
+      }
+    `;
+  }
+
+  async authenticate(ctx: RequestContext, data: KeycloakAuthData): Promise<User | false> {
+    const { data: userInfo } = await this.httpService
+      .get('http://localhost:9000/auth/realms/myrealm/protocol/openid-connect/userinfo', {
+          headers: {
+              Authorization: `Bearer ${data.token}`,
+          },
+      }).toPromise();
+
+    if (!userInfo) {
+        return false;
+    }
+    const user = await this.externalAuthenticationService.findAdministratorUser(this.name, userInfo.sub);
+    if (user) {
+        return user;
+    }
+
+    // When creating an Administrator, we need to know what Role(s) to assign.
+    // In this example, we've created a "merchant" role and assign that to all
+    // new Administrators. In a real implementation, you can have more complex
+    // logic to map an external user to a given role.
+    const roles = await this.roleService.findAll();
+    const merchantRole = roles.items.find((r) => r.code === 'merchant');
+    if (!merchantRole) {
+        Logger.error(`Could not find "merchant" role`);
+        return false;
+    }
+
+    return this.externalAuthenticationService.createAdministratorAndUser(ctx, {
+        strategy: this.name,
+        externalIdentifier: userInfo.sub,
+        identifier: userInfo.preferred_username,
+        emailAddress: userInfo.email,
+        firstName: userInfo.given_name,
+        lastName: userInfo.family_name,
+        roles: [merchantRole],
+    });
+  }
+}
+```

+ 1 - 1
docs/content/docs/developer-guide/configuration.md

@@ -132,7 +132,7 @@ export const config: VendureConfig = {
 
 ### Configuring authentication
 
-Authentication settings are configured with [`VendureConfig.authOptions`]({{< relref "auth-options" >}}). The most important setting here is whether the storefront client will use cookies or bearer tokens to manage user sessions. For more detail on this topic, see [the Authentication & Sessions guide]({{< relref "authentication-and-sessions" >}}).
+Authentication settings are configured with [`VendureConfig.authOptions`]({{< relref "auth-options" >}}). The most important setting here is whether the storefront client will use cookies or bearer tokens to manage user sessions. For more detail on this topic, see [the Managing Sessions guide]({{< relref "managing-sessions" >}}).
 
 The username and default password of the superadmin user can also be specified here. In production, it is advisable to use environment variables for these settings.
 

+ 2 - 2
docs/content/docs/storefront/authentication-and-sessions.md → docs/content/docs/storefront/managing-sessions.md

@@ -1,10 +1,10 @@
 ---
-title: "Authentication & Sessions"
+title: "Managing Sessions"
 weight: 1
 showtoc: true
 ---
  
-# Authentication & Sessions
+# Managing Sessions
 
 Vendure supports two ways to manage user sessions: cookies and bearer token. The method you choose depends on your requirements, and is specified by the [`authOptions.tokenMethod` property]({{< relref "auth-options" >}}#tokenmethod) of the VendureConfig.
 

+ 0 - 0
docs/content/docs/developer-guide/order-workflow/_index.md → docs/content/docs/storefront/order-workflow/_index.md


+ 0 - 0
docs/content/docs/developer-guide/order-workflow/order_class_diagram.png → docs/content/docs/storefront/order-workflow/order_class_diagram.png


+ 0 - 0
docs/content/docs/developer-guide/order-workflow/order_state_diagram.png → docs/content/docs/storefront/order-workflow/order_state_diagram.png


+ 1 - 1
docs/content/docs/storefront/shop-api-guide.md

@@ -31,7 +31,7 @@ There are a couple of query parameters which are valid for all GraphQL operation
 
 ## Order flow
 
-*See the [Order Workflow guide]({{< relref "/docs/developer-guide/order-workflow" >}}) for a detailed explanation of how Orders are handled in Vendure.*
+*See the [Order Workflow guide]({{< relref "order-workflow" >}}) for a detailed explanation of how Orders are handled in Vendure.*
 
 * {{< shop-api-operation operation="activeOrder" type="query" >}} returns the currently-active Order for the session.
 * {{< shop-api-operation operation="addItemToOrder" type="mutation" >}} adds an item to the order. The first time it is called will also create a new Order for the session.

+ 8 - 4
packages/core/src/config/auth/authentication-strategy.ts

@@ -9,13 +9,14 @@ import { User } from '../../entity/user/user.entity';
  * An AuthenticationStrategy defines how a User (which can be a Customer in the Shop API or
  * and Administrator in the Admin API) may be authenticated.
  *
+ * Real-world examples can be found in the [Authentication guide](/docs/developer-guide/authentication/).
+ *
  * @docsCategory auth
  */
 export interface AuthenticationStrategy<Data = unknown> extends InjectableStrategy {
     /**
      * @description
-     * The `name` property is used to create the `AuthenticationMethod` GraphQL enum
-     * used by the `authenticate` mutation.
+     * The name of the strategy, for example `'facebook'`, `'google'`, `'keycloak'`.
      */
     readonly name: string;
 
@@ -26,7 +27,9 @@ export interface AuthenticationStrategy<Data = unknown> extends InjectableStrate
      * of the strategy. The shape of the input object should match the generic `Data`
      * type argument.
      *
+     * @example
      * For example, given the following:
+     *
      * ```TypeScript
      * defineInputType() {
      *   return gql`
@@ -58,14 +61,15 @@ export interface AuthenticationStrategy<Data = unknown> extends InjectableStrate
     /**
      * @description
      * Used to authenticate a user with the authentication provider. This method
-     * will
+     * will implement the provider-specific authentication logic, and should resolve to either a
+     * {@link User} object on success, or `false` on failure.
      */
     authenticate(ctx: RequestContext, data: Data): Promise<User | false>;
 
     /**
      * @description
      * Called when a user logs out, and may perform any required tasks
-     * related to the user logging out.
+     * related to the user logging out with the external provider.
      */
     onLogOut?(user: User): Promise<void>;
 }