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

外部認証

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

なぜ External Auth を使うのか

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

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

External Auth を使う場合は、authorization code を保存するだけで済みます。Squid が OAuth2 のライフサイクルの残り(トークンの保存、更新、ユーザーごとの分離)を管理します。

Backend code
// ユーザーが同意した後、authorization code を保存
// Squid がトークンに交換し、安全に保存します
const externalAuth = this.squid.externalAuth('google_drive');
await externalAuth.saveAuthCode(authCode, userId);

// 後で、有効な access token を取得(期限切れなら自動更新)
const { accessToken } = await externalAuth.getAccessToken(userId);

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

概要

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

仕組み

  1. ユーザー承認: ユーザーをサードパーティの OAuth 同意画面へ誘導し、アプリに権限を付与してもらう
  2. トークン交換: フロントエンドが authorization code をバックエンド関数に渡して 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 を呼び出すExecutable 内の Secrets を使用

クイックスタート

前提条件

  1. OAuth2 が必要なコネクタが Squid Console で設定済みであること(例: Google DriveGoogle Calendar
  2. サードパーティサービスの OAuth2 credentials(Client ID、Client Secret)がコネクタ設定に追加されていること
  3. @squidcloud/backend パッケージがNPMからインストールされた Squid backend プロジェクトがあること

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

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

auth code を保存し、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);
}
}

ステップ 2: ユーザーを OAuth 同意画面へ誘導する

フロントエンドでサードパーティの authorization URL を構築し、ユーザーをリダイレクトします。必要なパラメータは OAuth provider によって異なります。

たとえば 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'); // refresh token を受け取るために必須

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

OAuth2 provider ごとに、authorization URL、scope、パラメータの要件が異なります。必ず provider の OAuth2 ドキュメントで正確な要件を確認してください。

ステップ 3: authorization code を保存する

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

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', // コネクタ ID
authCode,
userId
);
}
}

authorization code は 1 回限りで、通常は数分以内に期限切れになるため、速やかに交換してください。

ステップ 4: バックエンド関数で access token を使う

auth code を保存したら、任意のバックエンド関数で有効な 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 パラメータは、トークンを特定ユーザーに関連付けます。認証 provider の user ID など、アプリケーション全体で一貫した一意の識別子をユーザーごとに使用してください。

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

トークンのライフサイクル

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

ステージ何が起きるか
保存saveAuthCode() が authorization code を access token と refresh token に交換し、その後保存します
取得getAccessToken() が有効な access token を返し、必要に応じて自動的に更新します
自動更新トークンは期限切れ 30 秒以内になると更新されます。バックグラウンドジョブも 5 分ごとに先回りしてトークンを更新します
クリーンアップrefresh token が恒久的に無効(例: ユーザーが権限を取り消した)なトークンは自動的に削除されます

対応コネクタ

External Auth は OAuth2 準拠の任意のコネクタで動作します。現在対応しているコネクタには次が含まれます:

API リファレンス

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

エラーハンドリング

よくあるエラー

エラー原因解決策
Integration not foundconnectorId が設定済みのコネクタに一致しないSquid Console のコネクタ ID と一致していることを確認する
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 failedauthorization code が無効、期限切れ、または既に使用済み同意画面へリダイレクトして新しい authorization code を取得する
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')) {
// ユーザーがまだ認可していない
throw new Error('USER_NOT_AUTHORIZED');
} else if (error.message.includes('Refresh token has expired')) {
// ユーザーの再認可が必要
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')) {
// ユーザーを OAuth 同意画面へリダイレクト
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 呼び出しで、同じ一意の識別子(例: auth provider の user ID)を使用してください
  3. HTTPS のみを使用する: 本番環境の OAuth redirect URI では必ず HTTPS を使用してください
  4. scope を最小化する: アプリケーションに必要な OAuth scope のみを要求してください
  5. トークンは Squid に任せる: アプリケーション側でトークンを保存しないでください。常に getAccessToken を呼び出して、新しく有効なトークンを取得してください
  6. 再認可を丁寧に扱う: トークン更新が失敗したら、汎用的なエラーを表示するのではなく、OAuth フローを再度案内してください
  7. auth code は速やかに交換する: authorization code は 1 回限りで、すぐに期限切れになります。アプリにリダイレクトされたらできるだけ早く 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: ユーザーを Google の OAuth 同意画面へリダイレクト
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: OAuth コールバックを処理して 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: ユーザーの Drive ファイルを一覧表示
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
// ユーザーが Google Calendar の OAuth を完了した後
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);
}
}

// 直近のイベントを取得
async function getUpcomingEvents(userId: string) {
const events = await squid.executeFunction('getUpcomingEvents', userId);
console.log('Upcoming events:', events);
return events;
}

関連項目