External Authentication
third-party service connector 向けの OAuth2 token を管理します
External Auth を使用する理由
application が Google Drive や Google Calendar などの third-party service 内の user 固有 data に access する必要があります。各 user は OAuth2 を介して app を authorize する必要があり、これにより access token と refresh token が生成されます。これらの token は secure に保存し、expire 時に refresh し、user ごとに分離して保持する必要があります。
External Auth がない場合、application は OAuth2 token lifecycle 全体を自身で実装する必要があります。これには、authorization code から token への exchange、token の encryption と storage、expired token の refresh、credential cleanup の処理が含まれます。
External Auth では、authorization code を保存するだけです。token storage、refresh、user ごとの isolation を含む OAuth2 lifecycle の残りは Squid が管理します。
// Save the user's authorization code after they consent
// Squid exchanges it for tokens and stores them encrypted at rest
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);
token storage は不要です。refresh logic も不要です。auth code を渡すだけで access token を取得できます。
概要
External Authentication module は、third-party service connector 向けの OAuth2 authentication flow を処理する統一された方法を提供します。authorization code、access token、automatic token refresh を管理し、user に安全な authentication experience を提供します。
仕組み
- User authorization: user を third-party OAuth consent screen に誘導し、app に permission を付与してもらいます
- Token exchange: frontend が authorization code を backend function に渡し、backend function がこれを Squid に保存します。Squid は access token と refresh token に exchange します。
- Automatic management: Squid は token を user ごとに encrypted at rest で保存し、expire 前に自動的に refresh します
ステップ 1 は frontend で実装し、ステップ 2 では backend function を呼び出します。残りは Squid が処理します。
主な capability
- Secure token storage: token は encrypted at rest で、user ごとに安全に保存されます
- Automatic refresh: access token は expire 時に refresh されます(30 秒の buffer あり)。さらに、5 分ごとに background job により proactive に refresh されます
- Multi-user support: 各 user の token は unique identifier を使用して分離されます
- Connector agnostic: OAuth2-compliant service connector であれば任意のものに対応します
External Auth を使用する場合
| シナリオ | 推奨 |
|---|---|
| third-party OAuth2 service の user data に access する | External Auth |
| 独自 app に user を authenticate する | Authentication を使用 |
| shared API key で third-party API を呼び出す | Executable 内で Secrets を使用 |
クイックスタート
前提条件
- OAuth2 を必要とする Squid Console 内の構成済み connector(例: Google Drive、Google Calendar)
- connector configuration に追加された、third-party service の OAuth2 credential(Client ID、Client Secret)
- NPM から
@squidcloud/backendpackage がインストールされた Squid backend project
ステップ 1: External Auth 用の backend function を作成する
External Auth には Squid API key が必要であり、client-side code に含めてはいけません。代わりに、API key が server 上で安全に保たれる executables を介して、すべての External Auth logic を backend で実行します。frontend は OAuth redirect を処理し、これらの backend function を呼び出すだけにします。これらの backend function は必ず security rules を使用して保護してください。
auth code を保存し、access token を取得する service を作成します。
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: user を OAuth consent screen に誘導する
frontend で third-party authorization URL を構築し、user を redirect します。正確な parameter は 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'); // Required to receive a refresh token
window.location.href = authUrl.toString();
}
OAuth2 provider ごとに、authorization URL、scope、parameter の requirement は異なります。正確な requirement については、必ず provider の OAuth2 documentation を参照してください。
ステップ 3: Authorization code を保存する
user が consent を完了し、authorization code とともに app へ redirect された後、その code を backend function に渡します。
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
);
}
}
authorization code は single-use で通常数分以内に expire するため、すぐに exchange してください。
ステップ 4: Backend function で access token を使用する
auth code を保存すると、任意の backend function で valid access token を取得して external 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 }> };
}
token が expire 済み、または expire 間近の場合、Squid が自動的に refresh します。
コアコンセプト
User identifier
saveAuthCode および getAccessToken の identifier parameter は、token を特定 user に関連付けます。authentication provider の user ID など、application 全体で user ごとに一貫した unique identifier を使用してください。
各 user は OAuth flow を一度完了する必要があります。その後は、その user の identifier を使用していつでも valid access token を取得できます。
Token lifecycle
Squid は token lifecycle 全体を管理します。
| Stage | 処理内容 |
|---|---|
| Save | saveAuthCode() は authorization code を access token と refresh token に exchange し、encrypted at rest で保存します |
| Retrieve | getAccessToken() は valid access token を返し、必要に応じて自動的に refresh します |
| Auto-refresh | token は expire の 30 秒以内になると refresh されます。background job も 5 分ごとに token を proactive に refresh します |
| Cleanup | permanent に invalid な refresh token を持つ token(例: user が revoke した token)は自動的に削除されます |
サポートされる connector
External Auth は OAuth2-compliant connector であれば任意のものに対応します。現在サポートされる connector は次のとおりです。
- Google Drive: document および file への access
- Google Calendar: calendar event の管理
API Reference
API call の詳細は SDK reference docs で確認できます。たとえば、squid.externalAuth(connectorId) は、指定した connector の ExternalAuthClient を返します。
Error Handling
一般的な error
| Error | 原因 | 解決策 |
|---|---|---|
| Integration not found | connectorId が構成済み connector と一致しない | connector ID が Squid Console のものと一致することを確認する |
| External auth not supported for integration type | connector type が OAuth2 をサポートしていない | OAuth2 authentication をサポートする connector を使用する |
| No external auth tokens found | user が OAuth flow を完了する前に getAccessToken を呼び出した | user が authorization を完了し、saveAuthCode が呼び出されたことを確認する |
| Refresh token expired or invalid | user が access を revoke した、または refresh token が expire した | OAuth flow を再度完了して user を再 authorize するよう促す |
| OAuth token exchange failed | authorization code が invalid、expired、またはすでに使用済み | user を consent screen に redirect して、新しい authorization code を要求する |
| Missing client secret | connector に OAuth client secret がない | Squid Console の connector configuration に client secret を追加する |
Backend function での error 処理
@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 は backend 内に保持する: External Auth には Squid API key が必要であり、client-side code に公開してはいけません。すべての External Auth operation には backend executable を使用し、API key が server に保持されるようにしてください。frontend は OAuth redirect の処理と backend function の呼び出しだけを行うようにします。
- 一貫した identifier を使用する: 特定 user に対するすべての External Auth call で、同じ unique identifier(例: auth provider の user ID)を使用します
- HTTPS のみを使用する: production の OAuth redirect URI には常に HTTPS を使用します
- scope を最小化する: application が必要とする OAuth scope のみを要求します
- token の処理は Squid に任せる: token を独自 application に保存しないでください。常に
getAccessTokenを呼び出して fresh かつ valid な token を取得します - 再 authorization を適切に処理する: token refresh が失敗した場合、generic error を表示するのではなく、user を再度 OAuth flow に誘導します
- auth code をすぐに exchange する: authorization code は single-use で、すぐに expire します。user が app に redirect されたらすぐに
saveAuthCodeを呼び出してください
コード例
Google Drive: File の一覧表示
Google Drive file を一覧表示する backend function を含む完全な例です。
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: Upcoming event の取得
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 - client から呼び出す backend function
- Google Drive connector
- Google Calendar connector
- Connectors overview
- Authentication