Skip to main content

External Authentication

Manage OAuth2 tokens for third-party service connectors

Why Use External Auth

Your application needs to access user-specific data in third-party services such as Google Drive or Google Calendar. Each user must authorize your app via OAuth2, which produces access and refresh tokens that must be stored securely, refreshed when they expire, and kept isolated per user.

Without External Auth, your application must implement the full OAuth2 token lifecycle itself. This includes exchanging authorization codes for tokens, encrypting and storing them, refreshing expired tokens, and handling credential cleanup.

With External Auth, you only store the authorization code. Squid manages the rest of the OAuth2 lifecycle for you, including token storage, refresh, and per-user isolation.

Backend code
// Save the user's authorization code after they consent
// Squid exchanges it for tokens and stores them securely
const externalAuth = this.squid.externalAuth('google_drive');
await externalAuth.saveAuthCode(authCode, userId);

// Later, get a valid access token (auto-refreshed if expired)
const { accessToken } = await externalAuth.getAccessToken(userId);

No token storage. No refresh logic. Just auth codes in, access tokens out.

Overview

The External Authentication module provides a unified way to handle OAuth2 authentication flows for third-party service connectors. It manages authorization codes, access tokens, and automatic token refresh, providing a secure authentication experience for your users.

How it works

  1. User authorization: You direct the user to the third-party OAuth consent screen, where they grant your app permission
  2. Token exchange: The frontend passes the authorization code to a backend function, which saves it to Squid. Squid exchanges it for access and refresh tokens.
  3. Automatic management: Squid securely stores the tokens per user and automatically refreshes them before they expire

You implement step 1 on the frontend and call a backend function for step 2. Squid handles the rest.

Key capabilities

  • Secure token storage: Tokens are encrypted and stored securely per user
  • Automatic refresh: Access tokens are refreshed when they expire (with a 30-second buffer) and proactively via a background job every 5 minutes
  • Multi-user support: Each user's tokens are isolated using unique identifiers
  • Connector agnostic: Works with any OAuth2-compliant service connector

When to use External Auth

ScenarioRecommendation
Access user data in a third-party OAuth2 serviceExternal Auth
Authenticate users to your own appUse Authentication
Call a third-party API with a shared API keyUse Secrets in an Executable

Quick Start

Prerequisites

  1. A configured connector in the Squid Console that requires OAuth2 (e.g., Google Drive, Google Calendar)
  2. OAuth2 credentials from the third-party service (Client ID, Client Secret) added to the connector configuration
  3. A Squid backend project with the @squidcloud/backend package installed

Step 1: Create backend functions for External Auth

External Auth requires a Squid API key, which should not be included in client-side code. Instead, all External Auth logic runs in the backend via executables, where the API key is kept secure on the server. The frontend only handles the OAuth redirect and calls these backend functions. Be sure to secure these backend functions using security rules.

Create a service that saves auth codes and retrieves access tokens:

Backend code
import { executable, SquidService } from '@squidcloud/backend';

export class ExternalAuthService extends SquidService {
@executable()
async saveExternalAuthCode(
connectorId: string,
authCode: string,
userId: string
): Promise<{ accessToken: string; expirationTime: Date }> {
const externalAuth = this.squid.externalAuth(connectorId);
return externalAuth.saveAuthCode(authCode, userId);
}

@executable()
async getExternalAccessToken(
connectorId: string,
userId: string
): Promise<{ accessToken: string; expirationTime: Date }> {
const externalAuth = this.squid.externalAuth(connectorId);
return externalAuth.getAccessToken(userId);
}
}

On the frontend, construct the third-party authorization URL and redirect the user. The exact parameters depend on the OAuth provider.

For example, with Google:

Client code
function startOAuth() {
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', 'your-google-client-id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/oauth/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'https://www.googleapis.com/auth/drive.readonly');
authUrl.searchParams.set('access_type', 'offline'); // Required to receive a refresh token

window.location.href = authUrl.toString();
}
Provider Variations

Each OAuth2 provider has different requirements for authorization URLs, scopes, and parameters. Always consult your provider's OAuth2 documentation for exact requirements.

Step 3: Save the authorization code

After the user consents and is redirected back to your app with an authorization code, pass it to the backend function:

Client code
async function handleOAuthCallback(userId: string) {
const urlParams = new URLSearchParams(window.location.search);
const authCode = urlParams.get('code');

if (authCode) {
await squid.executeFunction(
'saveExternalAuthCode',
'google_drive', // Your connector ID
authCode,
userId
);
}
}

The authorization code is single-use and typically expires within minutes, so exchange it promptly.

Step 4: Use the access token in backend functions

With the auth code saved, you can now retrieve valid access tokens in any backend function to call external APIs:

Backend code
@executable()
async listFiles(userId: string): Promise<any> {
const externalAuth = this.squid.externalAuth('google_drive');
const { accessToken } = await externalAuth.getAccessToken(userId);

const response = await fetch('https://www.googleapis.com/drive/v3/files', {
headers: { Authorization: `Bearer ${accessToken}` },
});

return (await response.json()) as { files: Array<{ id: string; name: string }> };
}

Squid automatically refreshes the token if it has expired or is about to expire.

Core Concepts

User identifiers

The identifier parameter in saveAuthCode and getAccessToken associates tokens with a specific user. Use a consistent, unique identifier for each user across your application, such as their user ID from your authentication provider.

Each user must complete the OAuth flow once. After that, you can retrieve valid access tokens for that user at any time using their identifier.

Token lifecycle

Squid manages the full token lifecycle:

StageWhat happens
SavesaveAuthCode() exchanges the authorization code for access and refresh tokens, then stores them
RetrievegetAccessToken() returns a valid access token, refreshing it automatically if needed
Auto-refreshTokens are refreshed when within 30 seconds of expiration. A background job also refreshes tokens proactively every 5 minutes
CleanupTokens with permanently invalid refresh tokens (e.g., revoked by the user) are automatically deleted

Supported connectors

External Auth works with any OAuth2-compliant connector. Currently supported connectors include:

API Reference

Details about the API calls can be found in the SDK reference docs. Such as for squid.externalAuth(connectorId), which returns an ExternalAuthClient for the specified connector.

Error Handling

Common errors

ErrorCauseSolution
Integration not foundThe connectorId doesn't match a configured connectorVerify the connector ID matches the one in the Squid Console
External auth not supported for integration typeThe connector type doesn't support OAuth2Use a connector that supports OAuth2 authentication
No external auth tokens foundgetAccessToken called before the user completed the OAuth flowEnsure the user has completed authorization and saveAuthCode was called
Refresh token expired or invalidThe user revoked access or the refresh token expiredPrompt the user to re-authorize by completing the OAuth flow again
OAuth token exchange failedThe authorization code is invalid, expired, or already usedRequest a new authorization code by redirecting the user to the consent screen
Missing client secretThe connector is missing its OAuth client secretAdd the client secret to the connector configuration in the Squid Console

Handling errors in backend functions

Backend code
@executable()
async getExternalAccessToken(
connectorId: string,
userId: string
): Promise<{ accessToken: string; expirationTime: Date }> {
try {
const externalAuth = this.squid.externalAuth(connectorId);
return await externalAuth.getAccessToken(userId);
} catch (error: any) {
if (error.message.includes('No external auth tokens found')) {
// User hasn't authorized yet
throw new Error('USER_NOT_AUTHORIZED');
} else if (error.message.includes('Refresh token has expired')) {
// User needs to re-authorize
throw new Error('REAUTHORIZATION_REQUIRED');
}
throw error;
}
}
Client code
try {
const { accessToken } = await squid.executeFunction(
'getExternalAccessToken',
'google_drive',
userId
);
} catch (error: any) {
if (error.message.includes('USER_NOT_AUTHORIZED') ||
error.message.includes('REAUTHORIZATION_REQUIRED')) {
// Redirect user to the OAuth consent screen
startOAuth();
} else {
console.error('Failed to get access token:', error.message);
}
}

Best Practices

  1. Keep External Auth in the backend: External Auth requires a Squid API key, which should never be exposed in client-side code. Use backend executables for all External Auth operations so the API key stays on the server. The frontend should only handle the OAuth redirect and call backend functions.
  2. Use consistent identifiers: Use the same unique identifier (e.g., user ID from your auth provider) across all External Auth calls for a given user
  3. Use HTTPS only: Always use HTTPS in production for OAuth redirect URIs
  4. Minimize scopes: Request only the OAuth scopes your application needs
  5. Let Squid handle tokens: Do not store tokens in your own application. Always call getAccessToken to get a fresh, valid token
  6. Handle re-authorization gracefully: If a token refresh fails, guide the user through the OAuth flow again rather than showing a generic error
  7. Exchange auth codes promptly: Authorization codes are single-use and expire quickly. Call saveAuthCode as soon as the user is redirected back to your app

Code Examples

Google Drive: List files

A complete example with backend functions for listing Google Drive files:

Backend:

Backend code
import { executable, SquidService } from '@squidcloud/backend';

interface DriveFile {
id: string;
name: string;
mimeType: string;
}

export class DriveService extends SquidService {
private readonly CONNECTOR_ID = 'google_drive';

@executable()
async saveDriveAuthCode(
authCode: string,
userId: string
): Promise<{ accessToken: string; expirationTime: Date }> {
const externalAuth = this.squid.externalAuth(this.CONNECTOR_ID);
return externalAuth.saveAuthCode(authCode, userId);
}

@executable()
async listDriveFiles(userId: string): Promise<DriveFile[]> {
const externalAuth = this.squid.externalAuth(this.CONNECTOR_ID);
const { accessToken } = await externalAuth.getAccessToken(userId);

const response = await fetch(
'https://www.googleapis.com/drive/v3/files?pageSize=10',
{
headers: { Authorization: `Bearer ${accessToken}` },
}
);

if (!response.ok) {
throw new Error(`Google Drive API error: ${response.status}`);
}

const data = (await response.json()) as { files: DriveFile[] };
return data.files;
}
}

Frontend:

Client code
// Step 1: Redirect the user to Google's OAuth consent screen
function startGoogleDriveAuth() {
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', 'your-google-client-id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/oauth/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'https://www.googleapis.com/auth/drive.readonly');
authUrl.searchParams.set('access_type', 'offline');
authUrl.searchParams.set('prompt', 'consent');

window.location.href = authUrl.toString();
}

// Step 2: Handle the OAuth callback and save the auth code
async function handleOAuthCallback(userId: string) {
const urlParams = new URLSearchParams(window.location.search);
const authCode = urlParams.get('code');

if (!authCode) {
throw new Error('No authorization code received');
}

await squid.executeFunction('saveDriveAuthCode', authCode, userId);
}

// Step 3: List the user's Drive files
async function listDriveFiles(userId: string) {
const files = await squid.executeFunction('listDriveFiles', userId);
console.log('Files:', files);
return files;
}

Google Calendar: Fetch upcoming events

Backend:

Backend code
import { executable, SquidService } from '@squidcloud/backend';

interface CalendarEvent {
id: string;
summary: string;
start: { dateTime: string };
}

export class CalendarService extends SquidService {
private readonly CONNECTOR_ID = 'google_calendar';

@executable()
async saveCalendarAuthCode(
authCode: string,
userId: string
): Promise<{ accessToken: string; expirationTime: Date }> {
const externalAuth = this.squid.externalAuth(this.CONNECTOR_ID);
return externalAuth.saveAuthCode(authCode, userId);
}

@executable()
async getUpcomingEvents(userId: string): Promise<CalendarEvent[]> {
const externalAuth = this.squid.externalAuth(this.CONNECTOR_ID);
const { accessToken } = await externalAuth.getAccessToken(userId);

const now = new Date().toISOString();
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${now}&maxResults=10&orderBy=startTime&singleEvents=true`;

const response = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` },
});

if (!response.ok) {
throw new Error(`Google Calendar API error: ${response.status}`);
}

const data = (await response.json()) as { items: CalendarEvent[] };
return data.items;
}
}

Frontend:

Client code
// After the user completes OAuth for Google Calendar
async function handleCalendarCallback(userId: string) {
const urlParams = new URLSearchParams(window.location.search);
const authCode = urlParams.get('code');

if (authCode) {
await squid.executeFunction('saveCalendarAuthCode', authCode, userId);
}
}

// Fetch upcoming events
async function getUpcomingEvents(userId: string) {
const events = await squid.executeFunction('getUpcomingEvents', userId);
console.log('Upcoming events:', events);
return events;
}

See Also