外部認証
サードパーティサービスのコネクタ向けに OAuth2 トークンを管理する
External Auth を使う理由
アプリケーションは、Google Drive や Google Calendar などのサードパーティサービス内のユーザー固有データにアクセスする必要があります。各ユーザーは OAuth2 を通じてアプリを承認する必要があり、その結果として access token と refresh token が発行されます。これらのトークンは安全に保存し、期限切れ時に更新し、ユーザーごとに分離して管理する必要があります。
External Auth を使わない場合、アプリケーション側で OAuth2 のトークンライフサイクル全体を自前で実装する必要があります。これには、認可コードをトークンに交換する処理、暗号化と保存、期限切れトークンの更新、認証情報のクリーンアップの処理が含まれます。
External Auth を使う場合、保存するのは認可コードだけです。Squid が OAuth2 のライフサイクルの残りをすべて管理します。これには、トークンの保存、更新、ユーザーごとの分離が含まれます。
// 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、そして自動トークン更新を管理し、ユーザーに安全な認証体験を提供します。
仕組み
- ユーザー認可: ユーザーをサードパーティの OAuth 同意画面へ誘導し、アプリに権限を付与してもらう
- トークン交換: フロントエンドが認可コードをバックエンド関数に渡し、バックエンド関数がそれを Squid に保存する。Squid は認可コードを access token と refresh token に交換する
- 自動管理: Squid がユーザーごとにトークンを安全に保存し、有効期限前に自動で更新する
フロントエンドで手順 1 を実装し、手順 2 のためにバックエンド関数を呼び出します。残りは Squid が処理します。
主な機能
- 安全なトークン保存: トークンは暗号化され、ユーザーごとに安全に保存されます
- 自動更新: access token は期限切れ時に更新されます(30 秒のバッファ付き)。さらにバックグラウンドジョブにより 5 分ごとに先回りで更新されます
- マルチユーザー対応: 各ユーザーのトークンはユニークな識別子で分離されます
- コネクタ非依存: OAuth2 準拠の任意のサービスコネクタで動作します
External Auth を使うべきタイミング
| シナリオ | 推奨 |
|---|---|
| サードパーティ OAuth2 サービスのユーザーデータにアクセスする | External Auth |
| 自分のアプリにユーザーを認証する | Authentication を使用 |
| 共通の API key でサードパーティ API を呼び出す | Executable で Secrets を使用 |
クイックスタート
前提条件
- OAuth2 を必要とするよう設定されたコネクタが Squid Console にあること(例: Google Drive、Google Calendar)
- サードパーティサービスの OAuth2 クレデンシャル(Client ID、Client Secret)がコネクタ設定に追加されていること
@squidcloud/backendパッケージがインストールされた Squid backend プロジェクトがあること
Step 1: External Auth 用のバックエンド関数を作成する
External Auth には Squid API key が必要であり、これはクライアント側コードに含めるべきではありません。その代わり、External Auth のロジックはすべてバックエンド側で executables を通じて実行し、API key はサーバー上で安全に保管します。フロントエンドは OAuth リダイレクトを扱い、これらのバックエンド関数を呼び出すだけにします。これらのバックエンド関数は security rules を使って必ず保護してください。
認可コードの保存と access token の取得を行うサービスを作成します。
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 の場合:
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();
}
OAuth2 プロバイダごとに、認可 URL、スコープ、パラメータの要件は異なります。必ずプロバイダの OAuth2 ドキュメントを参照し、正確な要件を確認してください。
Step 3: 認可コードを保存する
ユーザーが同意し、認可コード付きでアプリにリダイレクトされたら、それをバックエンド関数へ渡します。
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 を呼び出せます。
@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 はトークンが期限切れ、または期限切れ直前の場合に自動で更新します。
コアコンセプト
ユーザー識別子
saveAuthCode と getAccessToken の identifier パラメータは、トークンを特定ユーザーに紐づけます。認証プロバイダの user ID など、アプリケーション全体で一貫したユニーク識別子を各ユーザーに使用してください。
各ユーザーは OAuth フローを 1 回完了する必要があります。その後は、識別子を使っていつでもそのユーザーの有効な access token を取得できます。
トークンライフサイクル
Squid はトークンライフサイクル全体を管理します。
| ステージ | 何が起こるか |
|---|---|
| Save | saveAuthCode() が認可コードを access token と refresh token に交換し、その後保存します |
| Retrieve | getAccessToken() が有効な access token を返します。必要に応じて自動で更新します |
| Auto-refresh | 期限切れまで 30 秒以内になるとトークンは更新されます。さらにバックグラウンドジョブが 5 分ごとに先回りで更新します |
| Cleanup | 永続的に無効な refresh token(例: ユーザーが取り消した)を持つトークンは自動的に削除されます |
対応コネクタ
External Auth は OAuth2 準拠の任意のコネクタで動作します。現在サポートされているコネクタには以下があります。
- Google Drive: ドキュメントとファイルへのアクセス
- Google Calendar: カレンダーイベント管理
API リファレンス
API 呼び出しの詳細は SDK reference docs にあります。たとえば、指定したコネクタに対する ExternalAuthClient を返す squid.externalAuth(connectorId) などです。
エラーハンドリング
よくあるエラー
| Error | 原因 | 解決策 |
|---|---|---|
| Integration not found | connectorId が設定済みコネクタと一致しない | 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 を追加する |
バックエンド関数でのエラー処理
@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;
}
}
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);
}
}
ベストプラクティス
- External Auth はバックエンドに置く: External Auth には Squid API key が必要であり、クライアント側コードで公開してはいけません。すべての External Auth 操作には backend executables を使い、API key をサーバー上に保持してください。フロントエンドは OAuth リダイレクトの処理とバックエンド関数の呼び出しだけを行います。
- 一貫した識別子を使う: あるユーザーに対するすべての External Auth 呼び出しで、同一のユニーク識別子(例: 認証プロバイダの user ID)を使用します
- HTTPS のみ使用する: 本番環境では OAuth redirect URI に必ず HTTPS を使用してください
- スコープを最小化する: アプリケーションに必要な OAuth スコープだけをリクエストしてください
- トークンは Squid に任せる: トークンを自分のアプリケーションに保存しないでください。常に
getAccessTokenを呼び出して、最新で有効なトークンを取得してください - 再認可を丁寧に扱う: トークン更新が失敗した場合、一般的なエラーを表示するのではなく、ユーザーを OAuth フローへ案内してください
- 認可コードは速やかに交換する: 認可コードは使い捨てで、すぐに期限切れになります。ユーザーがアプリに戻ってきたら、可能な限り早く
saveAuthCodeを呼び出してください
コード例
Google Drive: ファイル一覧を取得する
Google Drive のファイル一覧を取得するためのバックエンド関数を含む完全な例です。
Backend:
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:
// 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:
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:
// 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;
}
関連項目
- Executables - クライアントから呼び出されるバックエンド関数
- Google Drive connector
- Google Calendar connector
- Connectors overview
- Authentication