How to Create Custom Connectors for Social Login in Logto: A Complete Guide

To create a custom social connector in Logto, implement the SocialConnector interface from @logto/connector-kit by defining metadata, a Zod configuration schema, and four core functions (getAuthorizationUri, getUserInfo, getTokenResponseAndUserInfo, and optionally getAccessTokenByRefreshToken) in a new package under packages/connectors/.

Logto treats every social login integration as a connector — a self-contained package that bridges your application with external identity providers. According to the logto-io/logto source code, each connector lives under packages/connectors/connector-<name> and implements a standardized interface that the Logto Console discovers automatically. This architecture allows you to add support for any OAuth 2.0 or OpenID Connect provider without modifying Logto's core.

Understanding the SocialConnector Interface

The SocialConnector interface exported by @logto/connector-kit requires three structural components and four functional implementations.

Required Metadata and Configuration

Every connector must export a defaultMetadata object containing a unique id, display name, and logo paths. This metadata populates the Logto Console UI.

You must also define a configuration guard using Zod (z.object({ ... })) that validates admin-provided settings such as clientId, clientSecret, authorization endpoints, and scopes. The guard in packages/connectors/connector-oidc/types.ts demonstrates this pattern for OIDC providers.

Core Functions You Must Implement

Logto invokes these functions during the authentication flow:

  • getAuthorizationUri – Constructs the identity provider's authorization URL and stores a nonce in the session using setSession.
  • getUserInfo – Exchanges the authorization code for tokens, validates the ID token or calls a user-info endpoint, and returns a normalized profile.
  • getTokenResponseAndUserInfo – Returns both the raw token response and the user profile (required when storing tokens for downstream API access).
  • getAccessTokenByRefreshToken (optional) – Refreshes access tokens when Logto needs to call the IdP's APIs later.

The connector-kit provides helpers like constructAuthorizationUri for OAuth 2.0 URL building and jwtVerify for ID-token validation.

Step-by-Step Implementation Guide

1. Scaffold the Connector Package

Create a new directory at packages/connectors/connector-my-social. Add a package.json that inherits the shared template by running pnpm sync-preset from the templates folder (packages/connectors/templates/sync-preset.js).

This synchronization ensures consistent build scripts, metadata handling, and logo management across all connectors.

2. Define Metadata and Configuration Schema

Create src/constant.ts to export static metadata:

export const defaultMetadata = {
  id: 'my-social',
  name: 'My Social',
  logo: './logo.svg',
  logoDark: './logo-dark.svg',
};

Define your configuration schema in src/types.ts using Zod:

import { z } from 'zod';

export const mySocialConnectorConfigGuard = z.object({
  clientId: z.string(),
  clientSecret: z.string(),
  authorizationEndpoint: z.string().url(),
  tokenEndpoint: z.string().url(),
  userInfoEndpoint: z.string().url(),
  scope: z.string().default('openid profile email'),
  customConfig: z.record(z.string()).optional(),
});

export type MySocialConnectorConfig = z.infer<typeof mySocialConnectorConfigGuard>;

3. Implement Authorization and User Info Flows

In src/index.ts, implement the required functions. The connector-oidc/src/index.ts file serves as the canonical reference for handling nonce generation and ID-token validation.

Your implementation must:

  1. Generate a nonce using generateStandardId() and store it via setSession in getAuthorizationUri.
  2. Retrieve the nonce via getSession in getUserInfo to prevent CSRF attacks.
  3. Normalize the IdP's user profile to Logto's standard fields: id, name, avatar, email, and phone.

4. Export and Register the Connector

Export a factory function that assembles the connector object:

const createMySocialConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => ({
  metadata: defaultMetadata,
  type: ConnectorType.Social,
  configGuard: mySocialConnectorConfigGuard,
  getAuthorizationUri: getAuthorizationUri(getConfig),
  getUserInfo: getUserInfo(getConfig),
  getTokenResponseAndUserInfo: getTokenResponseAndUserInfo(getConfig),
  getAccessTokenByRefreshToken: getAccessTokenByRefreshToken(getConfig),
});

export default createMySocialConnector;

Run pnpm prepack to synchronize presets and build the package. The connector automatically appears in the Logto Console under Sign-in experience → Social sign-in.

Complete Code Example

Below is a production-ready skeleton mirroring the OIDC connector implementation in packages/connectors/connector-oidc/src/index.ts:

// packages/connectors/connector-my-social/src/index.ts
import {
  ConnectorError,
  ConnectorErrorCodes,
  ConnectorType,
  validateConfig,
} from '@logto/connector-kit';
import { constructAuthorizationUri } from '@logto/connector-oauth';
import { generateStandardId } from '@logto/shared/universal';
import { defaultMetadata } from './constant.js';
import { mySocialConnectorConfigGuard, type MySocialConnectorConfig } from './types.js';
import type {
  GetAuthorizationUri,
  GetUserInfo,
  SocialConnector,
  CreateConnector,
  GetConnectorConfig,
  GetTokenResponseAndUserInfo,
  GetAccessTokenByRefreshToken,
} from '@logto/connector-kit';

const generateNonce = () => generateStandardId();

const getAuthorizationUri: (g: GetConnectorConfig) => GetAuthorizationUri =
  (getConfig) => async ({ state, redirectUri, scope: customScope }, setSession) => {
    const cfg = await getConfig(defaultMetadata.id);
    validateConfig(cfg, mySocialConnectorConfigGuard);
    const { clientId, authorizationEndpoint, scope, customConfig } = cfg;

    const nonce = generateNonce();
    await setSession?.({ nonce, redirectUri });

    return constructAuthorizationUri(authorizationEndpoint, {
      responseType: 'code',
      clientId,
      scope: customScope ?? scope,
      redirectUri,
      state,
      nonce,
      ...customConfig,
    });
  };

const getUserInfo: (g: GetConnectorConfig) => GetUserInfo =
  (getConfig) => async (data, getSession) => {
    const cfg = await getConfig(defaultMetadata.id);
    validateConfig(cfg, mySocialConnectorConfigGuard);
    
    const { nonce, redirectUri } = await getSession?.() ?? {};
    // Implement getIdToken helper to exchange code for tokens
    const token = await getIdToken(cfg, data, redirectUri);
    const profile = await getUserInfoFromEndpoint(cfg.userInfoEndpoint, token.access_token);

    return {
      id: profile.sub,
      name: profile.name,
      avatar: profile.picture,
      email: profile.email,
      phone: profile.phone_number,
      rawData: profile,
    };
  };

const getTokenResponseAndUserInfo: (g: GetConnectorConfig) => GetTokenResponseAndUserInfo =
  (getConfig) => async (data, getSession) => {
    const token = await getIdToken(await getConfig(defaultMetadata.id), data, (await getSession?.())?.redirectUri);
    const userInfo = await getUserInfo(getConfig)(data, getSession);
    return { tokenResponse: token, userInfo };
  };

const getAccessTokenByRefreshToken: (g: GetConnectorConfig) => GetAccessTokenByRefreshToken =
  (getConfig) => async (refreshToken) => {
    const cfg = await getConfig(defaultMetadata.id);
    validateConfig(cfg, mySocialConnectorConfigGuard);
    
    const response = await fetch(cfg.tokenEndpoint, {
      method: 'POST',
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: cfg.clientId,
        client_secret: cfg.clientSecret,
        refresh_token: refreshToken,
      }),
    });
    
    if (!response.ok) {
      throw new ConnectorError(ConnectorErrorCodes.General, `Refresh failed: ${await response.text()}`);
    }
    return response.json();
  };

const createMySocialConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => ({
  metadata: defaultMetadata,
  type: ConnectorType.Social,
  configGuard: mySocialConnectorConfigGuard,
  getAuthorizationUri: getAuthorizationUri(getConfig),
  getUserInfo: getUserInfo(getConfig),
  getTokenResponseAndUserInfo: getTokenResponseAndUserInfo(getConfig),
  getAccessTokenByRefreshToken: getAccessTokenByRefreshToken(getConfig),
});

export default createMySocialConnector;

Replace the getIdToken and getUserInfoFromEndpoint placeholders with actual HTTP implementations using your preferred client (e.g., ky or fetch).

Key Reference Files in the Logto Repository

Summary

  • Custom social connectors in Logto are self-contained packages under packages/connectors/ that implement the SocialConnector interface.
  • Required components include metadata, a Zod config guard, and four core functions for authorization and user info retrieval.
  • Security best practices involve generating nonces in getAuthorizationUri and validating them in getUserInfo via the session helpers.
  • Registration occurs automatically after running pnpm prepack and restarting the Logto Console, where admins configure the connector using your defined schema.

Frequently Asked Questions

What is the minimum code required to create a custom social connector?

You need three files: constant.ts for metadata, types.ts for the Zod configuration guard, and index.ts exporting a factory function that returns an object with metadata, type: ConnectorType.Social, configGuard, getAuthorizationUri, and getUserInfo. The getTokenResponseAndUserInfo function is also required if you intend to store tokens for persistent API access.

How does Logto validate the configuration for custom connectors?

Logto uses the configGuard property you export, which must be a Zod schema. The validateConfig helper from @logto/connector-kit checks user input against this schema when admins save the connector settings in the Console, throwing ConnectorError with ConnectorErrorCodes.InvalidConfig if validation fails.

Can I reuse existing OAuth helpers when building a custom connector?

Yes. The @logto/connector-oauth package exports constructAuthorizationUri for building authorization URLs, and @logto/connector-kit provides jwtVerify for ID-token validation. The connector-oidc and connector-oauth2 packages in the Logto repository demonstrate how to integrate these helpers into your implementation.

Where should I store tokens returned by the identity provider?

Enable "Store tokens for persistent API access" in the Logto Console for your connector. When getTokenResponseAndUserInfo returns a tokenResponse, Logto encrypts and stores these tokens in the Secret Vault. You can retrieve them later via the Logto Management API or use them to make authenticated calls to the IdP's APIs on behalf of the user.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →