外部認証
サードパーティサービスコネクタ向けの OAuth2 トークンを管理
なぜ External Auth を使うのか
アプリケーションは、Google Drive や Google Calendar などのサードパーティサービスにあるユーザー固有のデータへアクセスする必要があります。各ユーザーは OAuth2 を介してアプリを承認する必要があり、その結果として access token と refresh token が発行されます。これらは安全に保存し、有効期限が切れたら更新し、ユーザーごとに分離して管理する必要があります。
External Auth がない場合、アプリケーション側で OAuth2 トークンのライフサイクル全体を自前で実装する必要があります。これには、authorization code をトークンに交換する処理、暗号化して保存する処理、期限切れトークンの更新、認証情報のクリーンアップ処理が含まれます。
External Auth を使う場合は、authorization code を保存するだけで済みます。Squid が OAuth2 のライフサイクルの残り(トークンの保存、更新、ユーザーごとの分離)を管理します。
// ユーザーが同意した後、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、自動トークン更新を管理し、ユーザーに安全な認証体験を提供します。
仕組み
- ユーザー承認: ユーザーをサードパーティの OAuth 同意画面へ誘導し、アプリに権限を付与してもらう
- トークン交換: フロントエンドが authorization code をバックエンド関数に渡して 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 credentials(Client ID、Client Secret)がコネクタ設定に追加されていること
@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 を取得するサービスを作成します:
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 の場合:
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();
}
OAuth2 provider ごとに、authorization URL、scope、パラメータの要件が異なります。必ず provider の OAuth2 ドキュメントで正確な要件を確認してください。
ステップ 3: authorization code を保存する
ユーザーが同意し、authorization 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 を呼び出せます:
@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 パラメータは、トークンを特定ユーザーに関連付けます。認証 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 準拠の任意のコネクタで動作します。現在対応しているコネクタには次が含まれます:
- Google Drive: ドキュメントおよびファイルへのアクセス
- Google Calendar: カレンダーイベントの管理
API リファレンス
API 呼び出しの詳細は SDK reference docs にあります。たとえば squid.externalAuth(connectorId) は、指定したコネクタの ExternalAuthClient を返します。
エラーハンドリング
よくあるエラー
| エラー | 原因 | 解決策 |
|---|---|---|
| Integration not found | connectorId が設定済みのコネクタに一致しない | 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 failed | authorization code が無効、期限切れ、または既に使用済み | 同意画面へリダイレクトして新しい authorization code を取得する |
| 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')) {
// ユーザーがまだ認可していない
throw new Error('USER_NOT_AUTHORIZED');
} else if (error.message.includes('Refresh token has expired')) {
// ユーザーの再認可が必要
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')) {
// ユーザーを OAuth 同意画面へリダイレクト
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 呼び出しで、同じ一意の識別子(例: auth provider の user ID)を使用してください
- HTTPS のみを使用する: 本番環境の OAuth redirect URI では必ず HTTPS を使用してください
- scope を最小化する: アプリケーションに必要な OAuth scope のみを要求してください
- トークンは Squid に任せる: アプリケーション側でトークンを保存しないでください。常に
getAccessTokenを呼び出して、新しく有効なトークンを取得してください - 再認可を丁寧に扱う: トークン更新が失敗したら、汎用的なエラーを表示するのではなく、OAuth フローを再度案内してください
- auth code は速やかに交換する: authorization code は 1 回限りで、すぐに期限切れになります。アプリにリダイレクトされたらできるだけ早く
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: ユーザーを 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:
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:
// ユーザーが 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;
}
関連項目
- Executables - クライアントから呼び出されるバックエンド関数
- Google Drive connector
- Google Calendar connector
- Connectors overview
- Authentication