プラグイン能力
Authrimプラグインはハンドラー実装を通じて能力を提供します。このガイドでは各能力タイプを詳しく説明します。
能力カテゴリ
| カテゴリ | プレフィックス | 登録メソッド |
|---|---|---|
| Notifier | notifier. | registry.registerNotifier() |
| IDプロバイダー | idp. | registry.registerIdP() |
| Authenticator | authenticator. | registry.registerAuthenticator() |
| Flow | flow. | Coming soon |
Notifier Handler
Notifierプラグインはメール、SMS、Push、またはカスタムチャネルを通じて通知を送信します。
インターフェース
interface NotifierHandler { /** 通知を送信 */ send(notification: Notification): Promise<SendResult>;
/** ハンドラーが指定されたオプションをサポートするか確認(オプション) */ supports?(options: NotificationOptions): boolean;}Notificationオブジェクト
interface Notification { channel: string; // 'email', 'sms', 'push', 'custom' to: string; // 受信者アドレス from?: string; // 送信者(オプション、デフォルトを使用) subject?: string; // 件名(メール用) body: string; // メッセージ本文 replyTo?: string; // 返信先アドレス(メール) cc?: string[]; // CCの受信者(メール) bcc?: string[]; // BCCの受信者(メール) templateId?: string; // テンプレート識別子 templateVars?: Record<string, unknown>; // テンプレート変数 metadata?: Record<string, unknown>; // カスタムメタデータ}SendResultオブジェクト
interface SendResult { success: boolean; // 送信が成功したか messageId?: string; // プロバイダーのメッセージID error?: string; // 失敗時のエラーメッセージ errorCode?: string; // プロバイダーのエラーコード retryable?: boolean; // リトライ可能か? providerResponse?: unknown; // 生のプロバイダーレスポンス}例:Console Notifier
ビルトインのConsole Notifierの簡略化バージョン:
import { z } from 'zod';import type { AuthrimPlugin, Notification, SendResult } from '@authrim/ar-lib-plugin';
const configSchema = z.object({ prefix: z.string().default('[NOTIFY]').describe('ログのプレフィックス'), logLevel: z.enum(['debug', 'info', 'warn']).default('info'),});
type ConsoleConfig = z.infer<typeof configSchema>;
export const consoleNotifierPlugin: AuthrimPlugin<ConsoleConfig> = { id: 'notifier-console', version: '1.0.0', capabilities: ['notifier.email', 'notifier.sms', 'notifier.push'], configSchema,
meta: { name: 'Console Notifier', description: '通知をコンソールに出力(開発専用)', category: 'notification', icon: 'terminal', },
register(registry, config) { const handler = { async send(notification: Notification): Promise<SendResult> { const messageId = `console-${Date.now()}`;
console.log(`${config.prefix} Notification sent:`, { messageId, channel: notification.channel, to: notification.to, subject: notification.subject, bodyPreview: notification.body.slice(0, 100), });
return { success: true, messageId }; },
supports(): boolean { return true; // すべてのチャネルをサポート }, };
// サポートするすべてのチャネルに登録 registry.registerNotifier('email', handler); registry.registerNotifier('sms', handler); registry.registerNotifier('push', handler); },};例:Resendを使用したEmail Notifier
import { z } from 'zod';import type { AuthrimPlugin, Notification, SendResult } from '@authrim/ar-lib-plugin';
const configSchema = z.object({ apiKey: z.string().min(1).describe('Resend APIキー'), defaultFrom: z.string().email().describe('デフォルトの送信元メール'), replyTo: z.string().email().optional().describe('返信先アドレス'), timeout: z.number().default(10000).describe('リクエストタイムアウト(ms)'),});
type ResendConfig = z.infer<typeof configSchema>;
export const resendPlugin: AuthrimPlugin<ResendConfig> = { id: 'notifier-resend', version: '1.0.0', capabilities: ['notifier.email'], configSchema,
meta: { name: 'Resend Email', description: 'Resend API経由でトランザクションメールを送信', category: 'notification', icon: 'mail', },
register(registry, config) { registry.registerNotifier('email', { async send(notification: Notification): Promise<SendResult> { try { const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: notification.from || config.defaultFrom, to: notification.to, subject: notification.subject || 'Notification', html: notification.body, reply_to: notification.replyTo || config.replyTo, }), signal: AbortSignal.timeout(config.timeout), });
const result = await response.json();
if (!response.ok) { return { success: false, error: result.message || 'API error', errorCode: result.statusCode?.toString(), retryable: response.status >= 500, }; }
return { success: true, messageId: result.id, providerResponse: result, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error', retryable: true, }; } }, }); },};IDプロバイダー Handler
IdPプラグインはソーシャルログインとフェデレーション認証を可能にします。
インターフェース
interface IdPHandler { /** OAuthフロー用の認可URLを生成 */ getAuthorizationUrl(params: IdPAuthParams): Promise<string>;
/** 認可コードをトークンに交換 */ exchangeCode(params: IdPExchangeParams): Promise<IdPTokenResult>;
/** アクセストークンを使用してユーザー情報を取得 */ getUserInfo(accessToken: string): Promise<IdPUserInfo>;
/** IDトークンを検証してデコード(オプション) */ validateIdToken?(idToken: string): Promise<IdPClaims>;}パラメータタイプ
interface IdPAuthParams { redirectUri: string; state: string; nonce?: string; scopes?: string[]; extraParams?: Record<string, string>;}
interface IdPExchangeParams { code: string; redirectUri: string; codeVerifier?: string; // PKCE用}
interface IdPTokenResult { accessToken: string; refreshToken?: string; idToken?: string; expiresIn?: number; tokenType: string; scope?: string;}
interface IdPUserInfo { sub: string; // サブジェクト識別子(一意のユーザーID) email?: string; emailVerified?: boolean; name?: string; givenName?: string; familyName?: string; picture?: string; locale?: string; [key: string]: unknown; // 追加のクレーム}OAuth 2.0フロー
sequenceDiagram
participant User
participant App
participant Authrim
participant IdP as Identity Provider
User->>App: "Xでサインイン"をクリック
App->>Authrim: OAuth開始
Authrim->>IdP: getAuthorizationUrl()
IdP-->>User: ログインにリダイレクト
User->>IdP: 認証
IdP-->>Authrim: コードでリダイレクト
Authrim->>IdP: exchangeCode()
IdP-->>Authrim: アクセストークン
Authrim->>IdP: getUserInfo()
IdP-->>Authrim: ユーザープロフィール
Authrim-->>App: セッション作成
例:GitHub IdPプラグイン
import { z } from 'zod';import type { AuthrimPlugin, IdPAuthParams, IdPExchangeParams } from '@authrim/ar-lib-plugin';
const configSchema = z.object({ clientId: z.string().min(1).describe('GitHub OAuth AppのClient ID'), clientSecret: z.string().min(1).describe('GitHub OAuth AppのClient Secret'), scopes: z.array(z.string()).default(['read:user', 'user:email']),});
type GitHubConfig = z.infer<typeof configSchema>;
export const githubIdpPlugin: AuthrimPlugin<GitHubConfig> = { id: 'idp-github', version: '1.0.0', capabilities: ['idp.github'], configSchema,
meta: { name: 'GitHub', description: 'GitHubでサインイン', category: 'identity', icon: 'github', },
register(registry, config) { registry.registerIdP('github', { async getAuthorizationUrl(params: IdPAuthParams): Promise<string> { const url = new URL('https://github.com/login/oauth/authorize'); url.searchParams.set('client_id', config.clientId); url.searchParams.set('redirect_uri', params.redirectUri); url.searchParams.set('state', params.state); url.searchParams.set('scope', (params.scopes ?? config.scopes).join(' ')); return url.toString(); },
async exchangeCode(params: IdPExchangeParams) { const response = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ client_id: config.clientId, client_secret: config.clientSecret, code: params.code, redirect_uri: params.redirectUri, }), });
const result = await response.json();
if (result.error) { throw new Error(result.error_description || result.error); }
return { accessToken: result.access_token, tokenType: result.token_type || 'bearer', scope: result.scope, }; },
async getUserInfo(accessToken: string) { const userResponse = await fetch('https://api.github.com/user', { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', }, });
if (!userResponse.ok) { throw new Error(`GitHub API error: ${userResponse.status}`); }
const user = await userResponse.json();
// プライマリメールを取得 let email: string | undefined; let emailVerified = false;
const emailResponse = await fetch('https://api.github.com/user/emails', { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', }, });
if (emailResponse.ok) { const emails = await emailResponse.json(); const primary = emails.find((e: any) => e.primary); if (primary) { email = primary.email; emailVerified = primary.verified; } }
return { sub: user.id.toString(), email, emailVerified, name: user.name || user.login, picture: user.avatar_url, }; }, }); },};Authenticator Handler
Authenticatorプラグインは MFA と認証方式を提供します。
インターフェース
interface AuthenticatorHandler { /** 認証チャレンジを開始 */ startChallenge(params: AuthChallengeParams): Promise<AuthChallengeResult>;
/** ユーザーのレスポンスを検証 */ verifyResponse(params: AuthVerifyParams): Promise<AuthVerifyResult>;
/** ユーザーがこの認証器を利用可能か確認(オプション) */ isAvailable?(userId: string): Promise<boolean>;}パラメータタイプ
interface AuthChallengeParams { userId: string; sessionId: string; metadata?: Record<string, unknown>;}
interface AuthChallengeResult { challengeId: string; challenge: unknown; // 認証器固有のデータ expiresAt: number; // Unixタイムスタンプ metadata?: Record<string, unknown>;}
interface AuthVerifyParams { challengeId: string; response: unknown; // ユーザーのレスポンス userId: string; sessionId: string;}
interface AuthVerifyResult { success: boolean; credentialId?: string; error?: string; metadata?: Record<string, unknown>;}例:TOTP Authenticator
ビルトインTOTPプラグインの簡略化バージョン:
import { z } from 'zod';import type { AuthrimPlugin } from '@authrim/ar-lib-plugin';
const configSchema = z.object({ issuer: z.string().default('Authrim').describe('認証アプリに表示される発行者名'), digits: z.literal(6).or(z.literal(8)).default(6).describe('OTPコードの桁数'), period: z.number().int().min(15).max(120).default(30).describe('コード更新間隔(秒)'), window: z.number().int().min(0).max(5).default(1).describe('時刻ずれ許容範囲(±ステップ)'),});
type TOTPConfig = z.infer<typeof configSchema>;
export const totpPlugin: AuthrimPlugin<TOTPConfig> = { id: 'authenticator-totp', version: '1.0.0', capabilities: ['authenticator.totp'], configSchema,
meta: { name: 'TOTP Authenticator', description: '時刻ベースのワンタイムパスワード(RFC 6238)', category: 'authentication', icon: 'shield-check', },
register(registry, config) { registry.registerAuthenticator('totp', { async startChallenge(params) { const isSetup = params.metadata?.setup === true; const challengeId = crypto.randomUUID(); const expiresAt = Date.now() + 5 * 60 * 1000; // 5分
if (isSetup) { // 新規TOTPセットアップ用のシークレットを生成 const secret = generateSecret(); const otpauthUri = generateOtpauthUri({ secret, issuer: config.issuer, accountName: params.metadata?.email as string || params.userId, digits: config.digits, period: config.period, });
return { challengeId, challenge: { type: 'totp_setup', secret: base32Encode(secret), otpauthUri, digits: config.digits, period: config.period, }, expiresAt, }; }
// 検証チャレンジ return { challengeId, challenge: { type: 'totp_verify', digits: config.digits, period: config.period, }, expiresAt, }; },
async verifyResponse(params) { const { code, secret } = params.response as { code: string; secret?: string };
if (!code || !/^\d{6,8}$/.test(code)) { return { success: false, error: 'Invalid code format' }; }
// TOTPコードを検証(実装はWeb Crypto APIを使用) const isValid = await verifyTOTP(secret, code, { digits: config.digits, period: config.period, window: config.window, });
if (isValid) { return { success: true, credentialId: params.challengeId, }; }
return { success: false, error: 'Invalid code' }; },
async isAvailable(userId: string) { // ユーザーがTOTPを設定しているか確認 return true; }, }); },};
// ヘルパー関数(簡略化)function generateSecret(): Uint8Array { const secret = new Uint8Array(20); crypto.getRandomValues(secret); return secret;}
function base32Encode(data: Uint8Array): string { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let result = ''; // ... エンコードロジック return result;}
function generateOtpauthUri(params: any): string { const { secret, issuer, accountName, digits, period } = params; return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}?secret=${base32Encode(secret)}&issuer=${issuer}&algorithm=SHA1&digits=${digits}&period=${period}`;}
async function verifyTOTP(secret: string, code: string, options: any): Promise<boolean> { // Web Crypto APIを使用したTOTP検証 // ... 検証ロジック return true;}Flow Nodes
登録ルール
- 一意性: 各能力チャネルは1つのハンドラーのみ持てる
- 先着優先: 最初の登録が優先される
- 競合時エラー: 同じチャネルを再登録するとエラーがスローされる
// これはエラーをスローするregistry.registerNotifier('email', handler1);registry.registerNotifier('email', handler2); // Error: already registered