堅牢な認証:強固なTokenリトライロジックの実装
はじめに
スマートロック管理のようなセキュリティクリティカルなシステムでは、認証の失敗は致命的になる可能性があります。単一のネットワーク障害や一時的なサーバー過負荷によって、ユーザーが自分の物件にアクセスできなくなってはいけません。この記事では、セキュリティ保証を維持しながら一時的な障害を優雅に処理する、堅牢なtoken更新メカニズムの実装方法について探求します。
クリティカルシステムにおけるToken管理の課題
スマートロックシステムでは、継続的な認証の有効性が必要です。ユーザーが単純に再ログインできるWebアプリケーションとは異なり、物理的なアクセスシステムは認証をシームレスに処理しなければなりません。間違ったタイミングでのtoken失効は、ゲストが部屋に入れない、または緊急時に物件管理者がアクセスを失う可能性があります。
Circuit Breakingを組み合わせたExponential Backoffの実装
堅牢なtoken更新システムは複数の耐障害パターンを組み合わせます:
interface RetryConfig {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
}
class TokenManager {
private config: RetryConfig = {
maxAttempts: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
backoffMultiplier: 2
};
async refreshTokenWithRetry(): Promise<string> {
let lastError: Error;
for (let attempt = 0; attempt < this.config.maxAttempts; attempt++) {
try {
const token = await this.performTokenRefresh();
await this.validateTokenClaims(token);
return token;
} catch (error) {
lastError = error;
if (!this.isRetryableError(error) || attempt === this.config.maxAttempts - 1) {
throw error;
}
const delay = this.calculateBackoffDelay(attempt);
await this.sleep(delay);
}
}
throw lastError;
}
private isRetryableError(error: Error): boolean {
// 認証失敗ではなく、ネットワーク/サーバーエラーのみリトライ
return error instanceof NetworkError ||
error instanceof ServerError;
}
private calculateBackoffDelay(attempt: number): number {
const exponentialDelay = this.config.baseDelayMs *
Math.pow(this.config.backoffMultiplier, attempt);
// Thundering herdを防ぐためjitterを追加
const jitter = Math.random() * 0.1 * exponentialDelay;
return Math.min(
exponentialDelay + jitter,
this.config.maxDelayMs
);
}
}認証失敗時のState保持
チェックインプロセスのような重要な操作では、認証が一時的に失敗してもstate一貫性を維持する必要があります:
interface CheckInState {
propertyId: string;
guestId: string;
checkInTime: Date;
lockCodes: string[];
status: 'pending' | 'in_progress' | 'completed' | 'failed';
}
class ResilientCheckInManager {
private stateStorage = new SecureStateStorage();
async performCheckIn(request: CheckInRequest): Promise<void> {
const checkInId = this.generateCheckInId();
let state: CheckInState = {
propertyId: request.propertyId,
guestId: request.guestId,
checkInTime: new Date(),
lockCodes: [],
status: 'pending'
};
try {
await this.stateStorage.save(checkInId, state);
state.status = 'in_progress';
await this.stateStorage.update(checkInId, state);
// tokenの問題で失敗する可能性がある
const lockCodes = await this.generateLockCodes(request);
state.lockCodes = lockCodes;
state.status = 'completed';
await this.stateStorage.update(checkInId, state);
} catch (error) {
if (error instanceof AuthenticationError) {
// 全体の操作を失敗させずに、リトライのためのstateを保持
await this.scheduleRetry(checkInId, state);
throw new TransientCheckInError('チェックインは自動的にリトライされます');
}
state.status = 'failed';
await this.stateStorage.update(checkInId, state);
throw error;
}
}
private async scheduleRetry(checkInId: string, state: CheckInState): Promise<void> {
const retryJob = {
id: checkInId,
type: 'check_in_retry',
state,
scheduledAt: new Date(Date.now() + 30000) // 30秒遅延
};
await this.jobQueue.schedule(retryJob);
}
}冪等性による重複操作の防止
一時的な障害は重複操作につながる可能性があります。冪等性keyにより、操作を安全にリトライできることを保証します:
class IdempotentOperationManager {
private operationCache = new Map<string, OperationResult>();
async executeWithIdempotency<T>(
key: string,
operation: () => Promise<T>,
ttlMs: number = 300000 // 5分
): Promise<T> {
// 操作が既に完了しているかチェック
const cached = this.operationCache.get(key);
if (cached && !this.isExpired(cached, ttlMs)) {
return cached.result as T;
}
try {
const result = await operation();
// 成功した結果をキャッシュ
this.operationCache.set(key, {
result,
timestamp: Date.now(),
status: 'success'
});
return result;
} catch (error) {
// 認証エラーの失敗はキャッシュしない - リトライすべき
if (!(error instanceof AuthenticationError)) {
this.operationCache.set(key, {
result: error,
timestamp: Date.now(),
status: 'error'
});
}
throw error;
}
}
private isExpired(cached: OperationResult, ttlMs: number): boolean {
return Date.now() - cached.timestamp > ttlMs;
}
}包括的なエラー分類
すべてのエラーを同じ方法で処理すべきではありません。堅牢なシステムはエラータイプを区別します:
enum ErrorCategory {
AUTHENTICATION = 'auth',
AUTHORIZATION = 'authz',
NETWORK = 'network',
VALIDATION = 'validation',
BUSINESS_LOGIC = 'business',
SYSTEM = 'system'
}
class ErrorClassifier {
static classify(error: Error): ErrorCategory {
if (error.message.includes('401') || error.message.includes('invalid_token')) {
return ErrorCategory.AUTHENTICATION;
}
if (error.message.includes('403') || error.message.includes('insufficient_permissions')) {
return ErrorCategory.AUTHORIZATION;
}
if (error instanceof TypeError && error.message.includes('fetch')) {
return ErrorCategory.NETWORK;
}
return ErrorCategory.SYSTEM;
}
static isRetryable(category: ErrorCategory): boolean {
return [ErrorCategory.NETWORK, ErrorCategory.SYSTEM].includes(category);
}
static getRetryStrategy(category: ErrorCategory): RetryStrategy {
switch (category) {
case ErrorCategory.NETWORK:
return { maxAttempts: 3, baseDelay: 1000 };
case ErrorCategory.AUTHENTICATION:
return { maxAttempts: 1, baseDelay: 0 }; // 認証失敗はリトライしない
default:
return { maxAttempts: 2, baseDelay: 500 };
}
}
}監視とアラート
本番環境での耐障害性には可観測性が必要です:
class AuthenticationMetrics {
async recordTokenRefresh(success: boolean, attemptCount: number, duration: number): Promise<void> {
await this.metrics.increment('token_refresh_attempts', {
success: success.toString(),
attempt_count: attemptCount.toString()
});
await this.metrics.histogram('token_refresh_duration_ms', duration);
// 高い失敗率でアラート
if (!success && attemptCount >= 3) {
await this.alerting.sendAlert({
severity: 'high',
message: '最大リトライ回数後にtoken更新が失敗しました',
context: { attemptCount, duration }
});
}
}
}まとめ
セキュリティクリティカルなシステムのための堅牢な認証を構築するには、多層アプローチが必要です:
- スマートなリトライロジック:jitterを使ったexponential backoffを実装し、適切なエラータイプのみをリトライする
- State保持:認証失敗を跨いで操作stateを維持し、シームレスな回復を可能にする
- 冪等性:一意のkeyと結果キャッシュで重複操作を防ぐ
- エラー分類:異なる失敗モードを区別し、それぞれを適切に処理する
- 可観測性:失敗パターンを監視し、懸念される傾向についてアラートする
これらのパターンにより、一時的な認証の問題がユーザーエクスペリエンスやシステムセキュリティを損なわないことを保証します。アクセスが重要なスマートロックシステムでは、このような耐障害性が軽微な不便と重大な運用失敗の違いを意味する可能性があります。