Browse Source

docs: Add Google auth guide (#3759)

Co-authored-by: Housein Abo Shaar <76689341+GogoIsProgramming@users.noreply.github.com>
Housein Abo Shaar 5 months ago
parent
commit
3c1dd18fc6
1 changed files with 493 additions and 0 deletions
  1. 493 0
      docs/docs/guides/how-to/google-oauth-authentication/index.mdx

+ 493 - 0
docs/docs/guides/how-to/google-oauth-authentication/index.mdx

@@ -0,0 +1,493 @@
+---
+title: "Google OAuth Authentication"
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+:::info
+The complete source of the following example plugin can be found here: [example-plugins/google-auth-plugin](https://github.com/vendure-ecommerce/examples/tree/master/examples/shop-google-auth)
+:::
+
+**Google OAuth authentication** allows customers to sign in using their Google accounts, providing a seamless experience that eliminates the need for password-based registration.
+
+This is particularly valuable for **consumer-facing stores** where users prefer the convenience and security of Google's authentication system, or for **B2B platforms** where organizations use Google Workspace.
+
+This guide shows you how to **add Google OAuth support** to your Vendure store using a custom [AuthenticationStrategy](/reference/typescript-api/auth/authentication-strategy/) and Google Identity Services.
+
+An **AuthenticationStrategy** in Vendure defines how users can log in to your store. Learn more about [authentication in Vendure](/guides/core-concepts/auth/).
+
+## Creating the Plugin
+
+**First, use the Vendure CLI** to create a new plugin for Google authentication:
+
+```bash
+npx vendure add -p GoogleAuthPlugin
+```
+
+This creates a basic [plugin](/guides/developer-guide/plugins/) structure with the necessary files.
+
+## Installing Dependencies
+
+**Google authentication requires** the Google Auth Library for token verification:
+
+```bash
+npm install google-auth-library
+```
+
+This library handles ID token verification securely on the server side, ensuring the tokens received from Google are authentic.
+
+## Creating the Authentication Strategy
+
+**Now create the Google authentication strategy.** Unlike traditional OAuth flows that use authorization codes, Google Identity Services provides **ID tokens directly**, which we verify server-side:
+
+```ts title="src/plugins/google-auth-plugin/google-auth-strategy.ts"
+import {
+  AuthenticationStrategy,
+  ExternalAuthenticationService,
+  Injector,
+  Logger,
+  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 interface GoogleAuthOptions {
+  googleClientId: string;
+  onUserCreated?: (ctx: RequestContext, injector: Injector, user: User) => void;
+  onUserFound?: (ctx: RequestContext, injector: Injector, user: User) => void;
+}
+
+export class GoogleAuthStrategy implements AuthenticationStrategy<GoogleAuthData> {
+  readonly name = 'google';
+  private client: OAuth2Client;
+  private externalAuthenticationService: ExternalAuthenticationService;
+  private logger: Logger;
+  private injector: Injector;
+
+  constructor(private options: GoogleAuthOptions) {
+    // Initialize Google OAuth2Client for token verification
+    this.client = new OAuth2Client(options.googleClientId);
+    this.logger = new Logger();
+  }
+
+  init(injector: Injector) {
+    // Get services we'll use for customer management
+    this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
+    this.injector = injector;
+  }
+
+  defineInputType(): DocumentNode {
+    // Define the GraphQL input type for the authenticate mutation
+    return gql`
+      input GoogleAuthInput {
+        token: String!
+      }
+    `;
+  }
+
+  async authenticate(ctx: RequestContext, data: GoogleAuthData): Promise<User | false> {
+    try {
+      // Step 1: Verify the Google ID token
+      const ticket = await this.client.verifyIdToken({
+        idToken: data.token,
+        audience: this.options.googleClientId,
+      });
+
+      const payload = ticket.getPayload();
+
+      if (!payload || !payload.email) {
+        this.logger.error('Invalid Google token or missing email', 'GoogleAuthStrategy');
+        return false;
+      }
+
+      // Step 2: Check if this Google user already has a Vendure account
+      const existingUser = await this.externalAuthenticationService.findCustomerUser(
+        ctx,
+        this.name,
+        payload.sub, // Google's unique user ID
+      );
+
+      if (existingUser) {
+        // User exists, log them in
+        this.logger.verbose(`User found: ${existingUser.identifier}`, 'GoogleAuthStrategy');
+        this.options.onUserFound?.(ctx, this.injector, existingUser);
+        return existingUser;
+      }
+
+      // Step 3: Create a new customer account for first-time Google users
+      const createdUser = await this.externalAuthenticationService.createCustomerAndUser(ctx, {
+        strategy: this.name,
+        externalIdentifier: payload.sub, // Store Google user ID
+        verified: payload.email_verified || false, // Use Google's verification status
+        emailAddress: payload.email,
+        firstName: payload.given_name || 'Google',
+        lastName: payload.family_name || 'User',
+      });
+
+      this.options.onUserCreated?.(ctx, this.injector, createdUser);
+
+      return createdUser;
+    } catch (error) {
+      this.logger.error(`Google authentication failed: ${error.message}`, 'GoogleAuthStrategy');
+      return false;
+    }
+  }
+}
+```
+
+The strategy uses Google's [OAuth2Client](https://googleapis.dev/nodejs/google-auth-library/latest/classes/OAuth2Client.html) to verify ID tokens and Vendure's [ExternalAuthenticationService](/reference/typescript-api/auth/external-authentication-service/) to handle customer creation.
+
+Key differences from other OAuth flows:
+- **ID Token Verification**: Google provides signed JWT tokens that we verify directly
+- **No Code Exchange**: Unlike GitHub OAuth, there's no authorization code to exchange
+- **Email Verification**: We respect Google's email verification status
+- **Fallback Names**: Provides defaults if Google profile lacks name information
+
+## Registering the Strategy
+
+**Now update the generated plugin file** to register your authentication strategy:
+
+```ts title="src/plugins/google-auth-plugin/google-auth-plugin.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+import { GoogleAuthStrategy } from './google-auth-strategy';
+
+export interface GoogleAuthPluginOptions {
+  googleClientId: string;
+}
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  configuration: (config) => {
+    const options = GoogleAuthPlugin.options;
+    
+    if (options?.googleClientId) {
+      config.authOptions.shopAuthenticationStrategy.push(
+        new GoogleAuthStrategy({ googleClientId: options.googleClientId })
+      );
+    }
+    return config;
+  },
+})
+export class GoogleAuthPlugin {
+  static options: GoogleAuthPluginOptions;
+
+  static init(options: GoogleAuthPluginOptions) {
+    this.options = options;
+    return GoogleAuthPlugin;
+  }
+}
+```
+
+## Adding to Vendure Config
+
+**Add the plugin** to your Vendure configuration:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { GoogleAuthPlugin } from './plugins/google-auth-plugin/google-auth-plugin.plugin';
+
+export const config: VendureConfig = {
+  // ... other config
+  plugins: [
+    // ... other plugins
+    GoogleAuthPlugin.init({
+      googleClientId: process.env.GOOGLE_CLIENT_ID!,
+    }),
+  ],
+  // ... rest of config
+};
+```
+
+## Setting up Google OAuth App
+
+**Before you can test the integration,** you need to create a Google OAuth 2.0 Client:
+
+1. **Go to** the [Google Cloud Console](https://console.cloud.google.com/)
+2. **Create a new project** or select an existing one
+3. **Navigate to** **APIs & Services → Credentials**
+4. **Click** **"Create Credentials" → "OAuth 2.0 Client ID"**
+5. **Select** **"Web application"** as the application type
+6. **Configure the client:**
+   - **Name**: Your app name (e.g., "My Vendure Store")
+   - **Authorized JavaScript origins**: `http://localhost:3001`
+   - **Authorized redirect URIs**: `http://localhost:3001/sign-in`
+
+:::note
+The **localhost URLs** shown here are for **local development only.** In production, replace `localhost:3001` with your actual domain (e.g., `https://mystore.com`).
+:::
+
+7. **Click "Create"** and copy the Client ID
+
+**Add the client ID** to your environment:
+
+```bash title=".env"
+GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com
+```
+
+## Frontend Integration
+
+### Creating the Sign-in Component
+
+**For the frontend,** we'll use Google's official Identity Services library, which provides a **secure and user-friendly sign-in experience:**
+
+```typescript title="components/GoogleSignInButton.tsx"
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+
+const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
+
+declare global {
+  interface Window {
+    google: any;
+    handleCredentialResponse?: (response: any) => void;
+  }
+}
+
+export function GoogleSignInButton() {
+  const router = useRouter();
+  const [pending, setPending] = useState(false);
+
+  useEffect(() => {
+    // Define the callback function globally
+    window.handleCredentialResponse = async (response: any) => {
+      setPending(true);
+      try {
+        const result = await authenticateWithGoogle(response.credential);
+
+        if (result?.success) {
+          router.replace('/account');
+        } else {
+          console.error('Authentication failed:', result?.message);
+        }
+      } catch (error) {
+        console.error('Google authentication error:', error);
+      } finally {
+        setPending(false);
+      }
+    };
+
+    // Load Google Identity Services
+    if (!window.google && GOOGLE_CLIENT_ID) {
+      const script = document.createElement('script');
+      script.src = 'https://accounts.google.com/gsi/client';
+      script.async = true;
+      script.onload = () => {
+        window.google.accounts.id.initialize({
+          client_id: GOOGLE_CLIENT_ID,
+          callback: window.handleCredentialResponse,
+        });
+      };
+      document.head.appendChild(script);
+    }
+
+    return () => {
+      delete window.handleCredentialResponse;
+    };
+  }, [router]);
+
+  const handleGoogleSignIn = () => {
+    if (!GOOGLE_CLIENT_ID) {
+      console.error('Google Client ID not configured');
+      return;
+    }
+
+    if (window.google) {
+      window.google.accounts.id.prompt();
+    } else {
+      console.error('Google SDK not loaded');
+    }
+  };
+
+  return (
+    <button
+      onClick={handleGoogleSignIn}
+      disabled={pending}
+      className="flex items-center justify-center w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
+    >
+      {pending ? (
+        'Authenticating...'
+      ) : (
+        <>
+          <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
+            <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
+            <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
+            <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
+            <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
+          </svg>
+          Continue with Google
+        </>
+      )}
+    </button>
+  );
+}
+```
+
+### Creating the Authentication Function
+
+**Create a server action** to handle the Google authentication:
+
+```typescript title="actions/auth.ts"
+'use server';
+
+import { gql } from 'graphql-request';
+
+const AUTHENTICATE_MUTATION = gql`
+  mutation AuthenticateWithGoogle($input: AuthenticationInput!) {
+    authenticate(input: $input) {
+      ... on CurrentUser {
+        id
+        identifier
+        channels {
+          code
+          token
+          permissions
+        }
+      }
+      ... on InvalidCredentialsError {
+        authenticationError
+        errorCode
+        message
+      }
+      ... on NotVerifiedError {
+        errorCode
+        message
+      }
+    }
+  }
+`;
+
+export async function authenticateWithGoogle(token: string) {
+  try {
+    const result = await vendureClient.request(AUTHENTICATE_MUTATION, {
+      input: {
+        google: {
+          token
+        }
+      }
+    });
+
+    if (result.authenticate.__typename === 'CurrentUser') {
+      // Authentication successful
+      return { success: true, user: result.authenticate };
+    } else {
+      // Handle authentication error
+      return {
+        success: false,
+        message: result.authenticate.message
+      };
+    }
+  } catch (error) {
+    console.error('Google authentication error:', error);
+    return {
+      success: false,
+      message: 'Authentication failed'
+    };
+  }
+}
+```
+
+**Add your Google Client ID** to the frontend environment:
+
+```bash title=".env.local"
+NEXT_PUBLIC_GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com
+VENDURE_API_ENDPOINT=http://localhost:3000/shop-api
+```
+
+**The Google Identity Services flow works as follows:**
+
+1. **User clicks "Continue with Google"** → Google popup appears
+2. **User signs in with Google** → Google returns an ID token  
+3. **Frontend sends the token to Vendure** → Vendure verifies token with Google
+4. **If valid, Vendure creates/finds customer** → User is logged in
+
+## Using the GraphQL API
+
+**Once your plugin is running,** Google authentication will be **available in your shop API:**
+
+<Tabs>
+<TabItem value="Mutation" label="Mutation" default>
+
+```graphql
+mutation AuthenticateWithGoogle {
+  authenticate(input: {
+    google: {
+      token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
+    }
+  }) {
+    ... on CurrentUser {
+      id
+      identifier
+      channels {
+        code
+        token
+        permissions
+      }
+    }
+    ... on InvalidCredentialsError {
+      authenticationError
+      errorCode
+      message
+    }
+    ... on NotVerifiedError {
+      errorCode
+      message
+    }
+  }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "authenticate": {
+      "id": "1",
+      "identifier": "user@gmail.com",
+      "channels": [
+        {
+          "code": "__default_channel__",
+          "token": "session_token_here",
+          "permissions": ["Authenticated"]
+        }
+      ]
+    }
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+## Customer Data Management
+
+**Google-authenticated customers** are managed like any other Vendure [Customer](/reference/typescript-api/entities/customer/):
+
+- **Email**: Uses the user's actual Google email address
+- **Verification**: Inherits Google's email verification status
+- **External ID**: Google's unique user ID (`sub` claim) for future authentication
+- **Profile**: First and last names from Google profile, with fallbacks
+- **Security**: No password stored - authentication handled entirely by Google
+
+This means **Google users work seamlessly** with Vendure's [order management](/guides/core-concepts/orders/), [promotions](/guides/core-concepts/promotions/), and all customer workflows.
+
+## Testing the Integration
+
+**To test your Google OAuth integration:**
+
+1. **Start your Vendure server** with the plugin configured
+2. **Navigate to your storefront** and click "Continue with Google"  
+3. **Complete the Google OAuth flow** when prompted
+4. **Verify customer creation** in the Vendure Admin UI
+5. **Test repeat logins** to ensure existing customers are found correctly