メインコンテンツまでスキップ

外部認証

サードパーティサービスのコネクタ向けに OAuth2 トークンを管理する

External Auth を使う理由

アプリケーションは、Google Drive や Google Calendar などのサードパーティサービス内のユーザー固有データにアクセスする必要があります。各ユーザーは OAuth2 を通じてアプリを承認する必要があり、その結果として access token と refresh token が発行されます。これらのトークンは安全に保存し、期限切れ時に更新し、ユーザーごとに分離して管理する必要があります。

External Auth を使わない場合、アプリケーション側で OAuth2 のトークンライフサイクル全体を自前で実装する必要があります。これには、認可コードをトークンに交換する処理、暗号化と保存、期限切れトークンの更新、認証情報のクリーンアップの処理が含まれます。

External Auth を使う場合、保存するのは認可コードだけです。Squid が OAuth2 のライフサイクルの残りをすべて管理します。これには、トークンの保存、更新、ユーザーごとの分離が含まれます。

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);

トークンの保存は不要。更新ロジックも不要。必要なのは認可コードを渡し、access token を受け取るだけです。

概要

External Authentication モジュールは、サードパーティサービスの connectors に対する OAuth2 認証フローを統一的に扱う方法を提供します。認可コード、access token、そして自動トークン更新を管理し、ユーザーに安全な認証体験を提供します。

仕組み

  1. ユーザー認可: ユーザーをサードパーティの OAuth 同意画面へ誘導し、アプリに権限を付与してもらう
  2. トークン交換: フロントエンドが認可コードをバックエンド関数に渡し、バックエンド関数がそれを Squid に保存する。Squid は認可コードを access token と refresh token に交換する
  3. 自動管理: Squid がユーザーごとにトークンを安全に保存し、有効期限前に自動で更新する

フロントエンドで手順 1 を実装し、手順 2 のためにバックエンド関数を呼び出します。残りは Squid が処理します。

主な機能

  • 安全なトークン保存: トークンは暗号化され、ユーザーごとに安全に保存されます
  • 自動更新: access token は期限切れ時に更新されます(30 秒のバッファ付き)。さらにバックグラウンドジョブにより 5 分ごとに先回りで更新されます
  • マルチユーザー対応: 各ユーザーのトークンはユニークな識別子で分離されます
  • コネクタ非依存: OAuth2 準拠の任意のサービスコネクタで動作します

External Auth を使うべきタイミング

シナリオ推奨
サードパーティ OAuth2 サービスのユーザーデータにアクセスするExternal Auth
自分のアプリにユーザーを認証するAuthentication を使用
共通の API key でサードパーティ API を呼び出すExecutableSecrets を使用

クイックスタート

前提条件

  1. OAuth2 を必要とするよう設定されたコネクタが Squid Console にあること(例: Google DriveGoogle Calendar
  2. サードパーティサービスの OAuth2 クレデンシャル(Client ID、Client Secret)がコネクタ設定に追加されていること
  3. @squidcloud/backend パッケージがインストールされた Squid backend プロジェクトがあること

Step 1: External Auth 用のバックエンド関数を作成する

External Auth には Squid API key が必要であり、これはクライアント側コードに含めるべきではありません。その代わり、External Auth のロジックはすべてバックエンド側で executables を通じて実行し、API key はサーバー上で安全に保管します。フロントエンドは OAuth リダイレクトを扱い、これらのバックエンド関数を呼び出すだけにします。これらのバックエンド関数は security rules を使って必ず保護してください。

認可コードの保存と access token の取得を行うサービスを作成します。

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);
}
}

Step 2: ユーザーを OAuth 同意画面へ誘導する

フロントエンド側でサードパーティの認可 URL を構築し、ユーザーをリダイレクトします。正確なパラメータは OAuth プロバイダによって異なります。

例として 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

OAuth2 プロバイダごとに、認可 URL、スコープ、パラメータの要件は異なります。必ずプロバイダの OAuth2 ドキュメントを参照し、正確な要件を確認してください。

Step 3: 認可コードを保存する

ユーザーが同意し、認可コード付きでアプリにリダイレクトされたら、それをバックエンド関数へ渡します。

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
);
}
}

認可コードは使い捨て(single-use)で、通常は数分以内に失効するため、速やかに交換してください。

Step 4: バックエンド関数で access token を使用する

認可コードが保存できたら、任意のバックエンド関数で有効な access token を取得して外部 API を呼び出せます。

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 はトークンが期限切れ、または期限切れ直前の場合に自動で更新します。

コアコンセプト

ユーザー識別子

saveAuthCodegetAccessTokenidentifier パラメータは、トークンを特定ユーザーに紐づけます。認証プロバイダの user ID など、アプリケーション全体で一貫したユニーク識別子を各ユーザーに使用してください。

各ユーザーは OAuth フローを 1 回完了する必要があります。その後は、識別子を使っていつでもそのユーザーの有効な access token を取得できます。

トークンライフサイクル

Squid はトークンライフサイクル全体を管理します。

ステージ何が起こるか
SavesaveAuthCode() が認可コードを access token と refresh token に交換し、その後保存します
RetrievegetAccessToken() が有効な access token を返します。必要に応じて自動で更新します
Auto-refresh期限切れまで 30 秒以内になるとトークンは更新されます。さらにバックグラウンドジョブが 5 分ごとに先回りで更新します
Cleanup永続的に無効な refresh token(例: ユーザーが取り消した)を持つトークンは自動的に削除されます

対応コネクタ

External Auth は OAuth2 準拠の任意のコネクタで動作します。現在サポートされているコネクタには以下があります。

API リファレンス

API 呼び出しの詳細は SDK reference docs にあります。たとえば、指定したコネクタに対する ExternalAuthClient を返す squid.externalAuth(connectorId) などです。

エラーハンドリング

よくあるエラー

Error原因解決策
Integration not foundconnectorId が設定済みコネクタと一致しないconnector ID が Squid Console のものと一致しているか確認する
External auth not supported for integration typeコネクタ種別が OAuth2 をサポートしていないOAuth2 認証をサポートするコネクタを使用する
No external auth tokens foundユーザーが OAuth フローを完了する前に getAccessToken が呼ばれたユーザーが認可を完了し、saveAuthCode が呼ばれたことを確認する
Refresh token expired or invalidユーザーがアクセスを取り消した、または refresh token が失効したOAuth フローを再度完了して再認可するようユーザーに促す
OAuth token exchange failed認可コードが無効、期限切れ、または既に使用済み同意画面へリダイレクトして新しい認可コードを取得する
Missing client secretコネクタに OAuth client secret が設定されていないSquid Console のコネクタ設定に client secret を追加する

バックエンド関数でのエラー処理

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);
}
}

ベストプラクティス

  1. External Auth はバックエンドに置く: External Auth には Squid API key が必要であり、クライアント側コードで公開してはいけません。すべての External Auth 操作には backend executables を使い、API key をサーバー上に保持してください。フロントエンドは OAuth リダイレクトの処理とバックエンド関数の呼び出しだけを行います。
  2. 一貫した識別子を使う: あるユーザーに対するすべての External Auth 呼び出しで、同一のユニーク識別子(例: 認証プロバイダの user ID)を使用します
  3. HTTPS のみ使用する: 本番環境では OAuth redirect URI に必ず HTTPS を使用してください
  4. スコープを最小化する: アプリケーションに必要な OAuth スコープだけをリクエストしてください
  5. トークンは Squid に任せる: トークンを自分のアプリケーションに保存しないでください。常に getAccessToken を呼び出して、最新で有効なトークンを取得してください
  6. 再認可を丁寧に扱う: トークン更新が失敗した場合、一般的なエラーを表示するのではなく、ユーザーを OAuth フローへ案内してください
  7. 認可コードは速やかに交換する: 認可コードは使い捨てで、すぐに期限切れになります。ユーザーがアプリに戻ってきたら、可能な限り早く saveAuthCode を呼び出してください

コード例

Google Drive: ファイル一覧を取得する

Google Drive のファイル一覧を取得するためのバックエンド関数を含む完全な例です。

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: 直近の予定を取得する

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;
}

関連項目