本番システムにおける堅牢な認証とエラー処理
はじめに
セキュリティが重要なシステムを構築するには、認証フローとエラー処理パターンに細心の注意を払う必要があります。最近の本番環境でのインシデントにより、堅牢な認証メカニズムと適切なエラー復旧の重要性が浮き彫りになっています。本記事では、適切な認証処理、ロールベースアクセス制御、および防御的プログラミング実践を通じて信頼できるシステムを構築するための主要なパターンを探ります。
認証状態管理
分散システムにおける認証では、不正アクセスを防ぎながらシステムの使いやすさを維持するために、慎重な状態管理が必要です。
OTP認証実装
interface AuthState {
step: 'initial' | 'otp-sent' | 'otp-verified' | 'authenticated';
userId?: string;
sessionToken?: string;
expiresAt?: Date;
}
class AuthenticationManager {
private state: AuthState = { step: 'initial' };
async initiateOTPLogin(email: string): Promise<void> {
try {
await this.sendOTP(email);
this.state = { step: 'otp-sent' };
} catch (error) {
this.handleAuthError(error);
throw new AuthenticationError('Failed to send OTP');
}
}
async verifyOTP(code: string): Promise<void> {
if (this.state.step !== 'otp-sent') {
throw new InvalidStateError('OTP verification not available');
}
const isValid = await this.validateOTP(code);
if (!isValid) {
throw new AuthenticationError('Invalid OTP code');
}
this.state = { step: 'otp-verified' };
}
}適切なAPI認証復旧
認証失敗を処理する際、システムはフォールバック機構を実装すべきです:
class APIClient {
async callWithFallback<T>(endpoint: string, data: any): Promise<T> {
try {
// プライマリ:Edge Functionを使用
return await this.callEdgeFunction(endpoint, data);
} catch (error) {
if (this.isAuthError(error)) {
// フォールバック:新しい認証情報で直接API呼び出し
const freshToken = await this.refreshAuthentication();
return await this.callDirectAPI(endpoint, data, freshToken);
}
throw error;
}
}
private isAuthError(error: any): boolean {
return error.status === 401 || error.status === 403;
}
}ロールベースアクセス制御
適切なロール管理は権限昇格を防ぎ、ユーザーが認可されたリソースにのみアクセスできるようにします。
動的ロールレベルチェック
interface Role {
id: string;
name: string;
level: number; // 数値が高い = より多くの権限
permissions: Permission[];
}
class RoleManager {
async checkAccess(userId: string, requiredLevel: number): Promise<boolean> {
try {
const userRole = await this.getUserRole(userId);
return userRole.level >= requiredLevel;
} catch (error) {
// フェイルクローズ - エラー時はアクセスを拒否
this.logger.error('Role check failed', { userId, error });
return false;
}
}
async saveRolePermissions(roleId: string, permissions: Permission[]): Promise<void> {
const transaction = await this.db.transaction();
try {
// 結果が返されない可能性があるクエリにはmaybeSingle()を使用
const existingRole = await transaction
.from('roles')
.select('*')
.eq('id', roleId)
.maybeSingle();
if (!existingRole) {
throw new NotFoundError(`Role ${roleId} not found`);
}
await transaction
.from('role_permissions')
.delete()
.eq('role_id', roleId);
await transaction
.from('role_permissions')
.insert(permissions.map(p => ({ role_id: roleId, permission_id: p.id })));
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
}データベースクエリの安全性
適切なクエリ処理により、実行時エラーを防ぎ、データの整合性を確保します。
安全なクエリパターン
class DatabaseService {
// レコードが存在しない可能性がある場合は.maybeSingle()を使用
async findUserRole(userId: string): Promise<Role | null> {
const result = await this.db
.from('user_roles')
.select('*')
.eq('user_id', userId)
.maybeSingle(); // レコードが見つからない場合はnullを返す
return result;
}
// レコードが必ず存在すべき場合のみ.single()を使用
async getUserById(userId: string): Promise<User> {
const result = await this.db
.from('users')
.select('*')
.eq('id', userId)
.single(); // レコードが見つからない場合は例外をスロー
if (!result) {
throw new NotFoundError(`User ${userId} not found`);
}
return result;
}
}初期化の安全性
システムは特に分散環境において、起動時の条件を適切に処理する必要があります。
安全なデータベース初期化
class DatabaseInitializer {
private isInitialized = false;
private initializationPromise?: Promise<void>;
async ensureInitialized(): Promise<void> {
if (this.isInitialized) {
return;
}
if (this.initializationPromise) {
return this.initializationPromise;
}
this.initializationPromise = this.initialize();
return this.initializationPromise;
}
private async initialize(): Promise<void> {
try {
await this.runMigrations();
await this.seedRequiredData();
await this.validateSystemHealth();
this.isInitialized = true;
} catch (error) {
this.initializationPromise = undefined;
throw new InitializationError('Database initialization failed', error);
}
}
private async validateSystemHealth(): Promise<void> {
const healthCheck = await this.db.raw('SELECT 1 as health');
if (!healthCheck) {
throw new Error('Database health check failed');
}
}
}決済セキュリティパターン
決済などの重要な操作には追加の安全対策が必要です。
状態駆動型決済フロー
interface PaymentState {
cardComplete: boolean;
processing: boolean;
validated: boolean;
}
class PaymentManager {
private state: PaymentState = {
cardComplete: false,
processing: false,
validated: false
};
updateCardInformation(cardData: Partial<CardInfo>): void {
const isComplete = this.validateCardCompleteness(cardData);
this.state.cardComplete = isComplete;
this.state.validated = isComplete && this.validateCardData(cardData);
}
canProcessPayment(): boolean {
return this.state.cardComplete &&
this.state.validated &&
!this.state.processing;
}
async processPayment(): Promise<void> {
if (!this.canProcessPayment()) {
throw new InvalidStateError('Payment cannot be processed');
}
this.state.processing = true;
try {
await this.chargeCard();
} finally {
this.state.processing = false;
}
}
}エラー分類と復旧
エラーの種類によって異なる処理戦略が必要です:
class ErrorHandler {
handleAPIError(error: APIError): void {
switch (error.status) {
case 401:
// 認証失敗 - 再認証をトリガー
this.triggerReAuthentication();
break;
case 403:
// 認可失敗 - セキュリティイベントをログ
this.logSecurityEvent('unauthorized_access_attempt', error);
break;
case 406:
// 受け入れ不可 - おそらくクエリの問題、フォールバックを使用
this.useFallbackQuery();
break;
default:
// 未知のエラー - 安全に失敗
this.escalateError(error);
}
}
private logSecurityEvent(event: string, context: any): void {
// セキュリティイベントの監査ログ
this.auditLogger.log({
event,
timestamp: new Date().toISOString(),
context,
severity: 'high'
});
}
}まとめ
信頼できるシステムの構築には以下が必要です:
- 防御的認証: 複数のフォールバック機構と適切な状態管理
- ロールベースセキュリティ: 動的レベルチェックとフェイルクローズアクセス制御
- 安全なデータベース操作: 適切なクエリメソッドとトランザクション安全性
- 初期化の安全性: 適切な起動処理とヘルス検証
- 決済セキュリティ: 状態駆動型フローと検証ゲート
- エラー復旧: 分類されたエラー処理と適切なフォールバック
これらのパターンにより、システムは予期しない状況や障害に直面しても、セキュアで信頼性の高い状態を維持できます。