Back to Vendure

GitHub OAuth Authentication

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

3.7.311.8 KB
Original Source

:::info The complete source of the following example plugin can be found here: example-plugins/github-auth-plugin :::

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.

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
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: Fetch the user's email addresses to determine the primary, verified email.
    // This requires the `user:email` OAuth scope. GitHub does NOT guarantee that a user's
    // email is verified, so we must read the `verified` flag rather than assume it.
    const emailsResponse = await fetch('https://api.github.com/user/emails', {
      headers: {
        'Authorization': `Bearer ${tokenData.access_token}`,
        'Accept': 'application/vnd.github.v3+json',
      },
    });
    const emails = (await emailsResponse.json()) as Array<{
      email: string;
      primary: boolean;
      verified: boolean;
    }>;
    const primaryEmail = Array.isArray(emails) ? emails.find(e => e.primary) : undefined;
    if (!primaryEmail) {
      throw new Error('Unable to retrieve a primary email address from GitHub');
    }

    // Step 4: 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 5: Create a new customer account for first-time GitHub users.
    // We pass `verified: primaryEmail.verified` so that this external identity is only
    // linked to a pre-existing account (one with the same email) when GitHub has actually
    // verified that the user owns the email. Passing `verified: true` unconditionally here
    // would risk account takeover — see the security note in the Authentication guide.
    const newCustomer = await this.externalAuthenticationService.createCustomerAndUser(ctx, {
      strategy: this.name,
      externalIdentifier: user.login, // Store GitHub username
      verified: primaryEmail.verified,
      emailAddress: primaryEmail.email,
      firstName: user.name?.split(' ')[0] || user.login,
      lastName: user.name?.split(' ').slice(1).join(' ') || '',
    });

    return newCustomer;
  }
}

The strategy uses Vendure's ExternalAuthenticationService to handle customer creation.

It uses the GitHub account's primary email address and only marks the account as verified when GitHub reports that email as verified, 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
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
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
  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
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
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);

  // `user:email` is required so the backend can read the primary email and its verified status.
  return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=read:user%20user:email&state=${state}`;
}

Handling the Callback

Create a callback handler to process the GitHub response and authenticate with Vendure:

typescript
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": "[email protected]",
      "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:

  • Email: The GitHub account's primary email address
  • Verification: Derived from GitHub's verified flag on the primary email — only verified emails mark the account as verified
  • 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, 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 Dashboard
  5. Check that subsequent logins find the existing customer account