コンテンツにスキップ

プラグイン能力

Authrimプラグインはハンドラー実装を通じて能力を提供します。このガイドでは各能力タイプを詳しく説明します。

能力カテゴリ

カテゴリプレフィックス登録メソッド
Notifiernotifier.registry.registerNotifier()
IDプロバイダーidp.registry.registerIdP()
Authenticatorauthenticator.registry.registerAuthenticator()
Flowflow.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. 一意性: 各能力チャネルは1つのハンドラーのみ持てる
  2. 先着優先: 最初の登録が優先される
  3. 競合時エラー: 同じチャネルを再登録するとエラーがスローされる
// これはエラーをスローする
registry.registerNotifier('email', handler1);
registry.registerNotifier('email', handler2); // Error: already registered

次のステップ

  • 設定スキーマ - 高度なスキーマパターンと機密フィールドの処理
  • デプロイ - プラグインのパッケージングと配布