Przeglądaj źródła

docs: Add Github SSO guide (#3758)

Co-authored-by: Housein Abo Shaar <76689341+GogoIsProgramming@users.noreply.github.com>
Housein Abo Shaar 5 miesięcy temu
rodzic
commit
5b5b1182e5

+ 359 - 0
docs/docs/guides/how-to/github-oauth-authentication/index.mdx

@@ -0,0 +1,359 @@
+---
+title: "GitHub 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/github-auth-plugin](https://github.com/vendure-ecommerce/examples/tree/master/examples/shop-github-auth)
+:::
+
+GitHub OAuth authentication allows customers to sign in using their GitHub accounts, eliminating the need for password-based registration.
+
+This is particularly valuable for developer-focused stores or B2B marketplaces.
+
+This guide shows you how to add GitHub OAuth support to your Vendure store using a custom [AuthenticationStrategy](/reference/typescript-api/auth/authentication-strategy/).
+
+## Creating the Plugin
+
+First, use the Vendure CLI to create a new plugin for GitHub authentication:
+
+```bash
+npx vendure add -p GitHubAuthPlugin
+```
+
+This creates a basic plugin structure with the necessary files.
+
+## Creating the Authentication Strategy
+
+Now create the GitHub authentication strategy. This handles the OAuth flow and creates customer accounts using GitHub profile data:
+
+```ts title="src/plugins/github-auth-plugin/github-auth-strategy.ts"
+import { AuthenticationStrategy, ExternalAuthenticationService, Injector, RequestContext, User } from '@vendure/core';
+import { DocumentNode } from 'graphql';
+import gql from 'graphql-tag';
+
+export interface GitHubAuthData {
+  code: string;
+  state: string;
+}
+
+export interface GitHubAuthOptions {
+  clientId: string;
+  clientSecret: string;
+}
+
+export class GitHubAuthenticationStrategy implements AuthenticationStrategy<GitHubAuthData> {
+  readonly name = 'github';
+  private externalAuthenticationService: ExternalAuthenticationService;
+
+  constructor(private options: GitHubAuthOptions) {}
+
+  init(injector: Injector) {
+    // Get the service we'll use to create/find customer accounts
+    this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
+  }
+
+  defineInputType(): DocumentNode {
+    // Define the GraphQL input type for the authenticate mutation
+    return gql`
+      input GitHubAuthInput {
+        code: String!
+        state: String!
+      }
+    `;
+  }
+
+  async authenticate(ctx: RequestContext, data: GitHubAuthData): Promise<User | false> {
+    const { code, state } = data;
+
+    // Step 1: Exchange the authorization code for an access token
+    const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
+      method: 'POST',
+      headers: {
+        'Accept': 'application/json',
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        client_id: this.options.clientId,
+        client_secret: this.options.clientSecret,
+        code,
+        state,
+      }),
+    });
+
+    const tokenData = await tokenResponse.json();
+    if (tokenData.error) {
+      throw new Error(`GitHub OAuth error: ${tokenData.error_description}`);
+    }
+
+    // Step 2: Use the access token to get user info from GitHub
+    const userResponse = await fetch('https://api.github.com/user', {
+      headers: {
+        'Authorization': `Bearer ${tokenData.access_token}`,
+        'Accept': 'application/vnd.github.v3+json',
+      },
+    });
+
+    const user = await userResponse.json();
+    if (!user.login) {
+      throw new Error('Unable to retrieve user information from GitHub');
+    }
+
+    // Step 3: Check if this GitHub user already has a Vendure account
+    const existingCustomer = await this.externalAuthenticationService.findCustomerUser(
+      ctx,
+      this.name,
+      user.login, // GitHub username as external identifier
+    );
+
+    if (existingCustomer) {
+      // User exists, log them in
+      return existingCustomer;
+    }
+
+    // Step 4: Create a new customer account for first-time GitHub users
+    const newCustomer = await this.externalAuthenticationService.createCustomerAndUser(ctx, {
+      strategy: this.name,
+      externalIdentifier: user.login, // Store GitHub username
+      verified: true, // GitHub accounts are pre-verified
+      emailAddress: `${user.login}-github@vendure.io`, // Unique email to avoid conflicts
+      firstName: user.name?.split(' ')[0] || user.login,
+      lastName: user.name?.split(' ').slice(1).join(' ') || '',
+    });
+
+    return newCustomer;
+  }
+}
+```
+
+The strategy uses Vendure's [ExternalAuthenticationService](/reference/typescript-api/auth/external-authentication-service/) to handle customer creation.
+
+It generates a unique email address for each GitHub user to avoid conflicts, and stores the GitHub username as the external identifier for future logins.
+
+## Registering the Strategy
+
+Now update the generated plugin file to register your authentication strategy:
+
+```ts title="src/plugins/github-auth-plugin/github-auth-plugin.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+import { GitHubAuthenticationStrategy, GitHubAuthOptions } from './github-auth-strategy';
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  configuration: config => {
+    config.authOptions.shopAuthenticationStrategy.push(new GitHubAuthenticationStrategy(GitHubAuthPlugin.options));
+    return config;
+  },
+})
+export class GitHubAuthPlugin {
+  static options: GitHubAuthOptions;
+
+  static init(options: GitHubAuthOptions) {
+    this.options = options;
+    return GitHubAuthPlugin;
+  }
+}
+```
+
+## Adding to Vendure Config
+
+Add the plugin to your Vendure configuration:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { GitHubAuthPlugin } from './plugins/github-auth-plugin/github-auth-plugin.plugin';
+
+export const config: VendureConfig = {
+  // ... other config
+  plugins: [
+    // ... other plugins
+    GitHubAuthPlugin.init({
+      clientId: process.env.GITHUB_CLIENT_ID!,
+      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
+    }),
+  ],
+  // ... rest of config
+};
+```
+
+## Setting up GitHub OAuth App
+
+Before you can test the integration, you need to create a GitHub OAuth App:
+
+1. Go to [GitHub Settings → Developer settings → OAuth Apps](https://github.com/settings/developers)
+2. Click "New OAuth App"
+3. Fill in the required fields:
+   - **Application name**: Your app name (e.g., "My Vendure Store")
+   - **Homepage URL**: `http://localhost:3001` (your storefront URL)
+   - **Authorization callback URL**: `http://localhost:3001/auth/github/callback`
+4. Click "Register application"
+5. Copy the Client ID and generate a Client Secret
+
+:::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`).
+:::
+
+Add these credentials to your environment:
+
+```bash title=".env"
+GITHUB_CLIENT_ID=your_github_client_id
+GITHUB_CLIENT_SECRET=your_github_client_secret
+```
+
+## Frontend Integration
+
+### Creating the Sign-in URL
+
+In your storefront, create a function to generate the GitHub authorization URL:
+
+```typescript title="utils/github-auth.ts"
+export function createGitHubSignInUrl(): string {
+  const clientId = process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID;
+  const redirectUri = encodeURIComponent('http://localhost:3001/auth/github/callback');
+  const state = Math.random().toString(36).substring(2);
+
+  // Store state for CSRF protection
+  sessionStorage.setItem('github_oauth_state', state);
+
+  return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=read:user&state=${state}`;
+}
+```
+
+### Handling the Callback
+
+Create a callback handler to process the GitHub response and authenticate with Vendure:
+
+```typescript title="pages/auth/github/callback.ts"
+import { gql } from 'graphql-request';
+
+const AUTHENTICATE_MUTATION = gql`
+  mutation Authenticate($input: GitHubAuthInput!) {
+    authenticate(input: { github: $input }) {
+      ... on CurrentUser {
+        id
+        identifier
+        channels {
+          code
+          token
+          permissions
+        }
+      }
+      ... on InvalidCredentialsError {
+        authenticationError
+        errorCode
+        message
+      }
+    }
+  }
+`;
+
+export async function handleGitHubCallback(code: string, state: string) {
+  // Verify CSRF protection
+  const storedState = sessionStorage.getItem('github_oauth_state');
+  if (state !== storedState) {
+    throw new Error('Invalid state parameter');
+  }
+
+  sessionStorage.removeItem('github_oauth_state');
+
+  // Authenticate with Vendure
+  const result = await vendureClient.request(AUTHENTICATE_MUTATION, {
+    input: { code, state }
+  });
+
+  if (result.authenticate.__typename === 'CurrentUser') {
+    // Authentication successful - redirect to account page
+    return result.authenticate;
+  } else {
+    // Handle authentication error
+    throw new Error(result.authenticate.message);
+  }
+}
+```
+
+The OAuth flow follows these steps:
+
+1. User clicks "Sign in with GitHub" → redirected to GitHub
+2. User authorizes your app → GitHub redirects back with code and state
+3. Your callback exchanges the code for user data → creates Vendure session
+
+## Using the GraphQL API
+
+Once your plugin is running, the GitHub authentication will be available in your shop API:
+
+<Tabs>
+<TabItem value="Mutation" label="Mutation" default>
+
+```graphql
+mutation AuthenticateWithGitHub {
+  authenticate(input: {
+    github: {
+      code: "authorization_code_from_github",
+      state: "csrf_protection_state"
+    }
+  }) {
+    ... on CurrentUser {
+      id
+      identifier
+      channels {
+        code
+        token
+        permissions
+      }
+    }
+    ... on InvalidCredentialsError {
+      authenticationError
+      errorCode
+      message
+    }
+  }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "authenticate": {
+      "id": "1",
+      "identifier": "github-user-github@vendure.io",
+      "channels": [
+        {
+          "code": "__default_channel__",
+          "token": "session_token_here",
+          "permissions": ["Authenticated"]
+        }
+      ]
+    }
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+## Customer Data Management
+
+GitHub-authenticated customers are managed like any other Vendure [Customer](/reference/typescript-api/entities/customer/):
+
+- **Email**: Generated as `{username}-github@vendure.io` to avoid conflicts
+- **Verification**: Automatically verified (GitHub handles email verification)
+- **External ID**: GitHub username stored for future authentication
+- **Profile**: Name extracted from GitHub profile when available
+
+This means GitHub users work seamlessly with Vendure's [order management](/guides/core-concepts/orders/), [promotions](/guides/core-concepts/promotions/), and customer workflows.
+
+## Testing the Integration
+
+To test your GitHub OAuth integration:
+
+1. Start your Vendure server with the plugin configured
+2. Navigate to your storefront and click the GitHub sign-in link
+3. Authorize your GitHub app when prompted
+4. Verify that a new customer is created in the Vendure Admin UI
+5. Check that subsequent logins find the existing customer account