Skip to content

Commit b372f4a

Browse files
Merge pull request #36 from Patrick-Ehimen/feat/auth-infrastructure
feat(auth): implement core authentication infrastructure
2 parents 3d8b3b7 + a7681f9 commit b372f4a

18 files changed

+1018
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* Authentication manager with API key validation and fallback logic
3+
*/
4+
5+
import { AuthConfig, ValidationResult, AuthenticationResult } from "./types.js";
6+
import { KeyValidationCache } from "./KeyValidationCache.js";
7+
import { RateLimiter } from "./RateLimiter.js";
8+
import { SecureKeyHandler } from "./SecureKeyHandler.js";
9+
10+
export class AuthManager {
11+
private config: AuthConfig;
12+
private cache: KeyValidationCache;
13+
private rateLimiter: RateLimiter;
14+
15+
constructor(config: AuthConfig) {
16+
this.config = config;
17+
this.cache = new KeyValidationCache(config.keyValidationCache);
18+
this.rateLimiter = new RateLimiter(config.rateLimiting);
19+
}
20+
21+
/**
22+
* Validate an API key
23+
*/
24+
async validateApiKey(apiKey: string): Promise<ValidationResult> {
25+
const startTime = Date.now();
26+
27+
// Validate format first
28+
if (!SecureKeyHandler.isValidFormat(apiKey)) {
29+
return {
30+
isValid: false,
31+
keyHash: SecureKeyHandler.hashKey(apiKey || "invalid"),
32+
errorMessage: "Invalid API key format",
33+
};
34+
}
35+
36+
const keyHash = SecureKeyHandler.hashKey(apiKey);
37+
38+
// Check cache first
39+
const cached = this.cache.get(keyHash);
40+
if (cached) {
41+
return cached;
42+
}
43+
44+
// Check rate limiting
45+
const rateLimitResult = this.rateLimiter.isAllowed(keyHash);
46+
if (!rateLimitResult.allowed) {
47+
const result: ValidationResult = {
48+
isValid: false,
49+
keyHash,
50+
errorMessage: "Rate limit exceeded",
51+
rateLimitInfo: {
52+
remaining: rateLimitResult.remaining,
53+
resetTime: rateLimitResult.resetTime,
54+
limit: this.config.rateLimiting.requestsPerMinute,
55+
},
56+
};
57+
return result;
58+
}
59+
60+
// Perform actual validation
61+
// In a real implementation, this would call the Lighthouse API to validate the key
62+
// For now, we'll do basic validation and assume the key is valid if it has the right format
63+
const isValid = await this.performKeyValidation(apiKey);
64+
65+
const result: ValidationResult = {
66+
isValid,
67+
keyHash,
68+
errorMessage: isValid ? undefined : "API key validation failed",
69+
rateLimitInfo: {
70+
remaining: rateLimitResult.remaining,
71+
resetTime: rateLimitResult.resetTime,
72+
limit: this.config.rateLimiting.requestsPerMinute,
73+
},
74+
};
75+
76+
// Cache the result if valid
77+
if (isValid) {
78+
this.cache.set(keyHash, result);
79+
}
80+
81+
return result;
82+
}
83+
84+
/**
85+
* Get effective API key (request key or fallback)
86+
*/
87+
async getEffectiveApiKey(requestKey?: string): Promise<string> {
88+
// If request key is provided, use it
89+
if (requestKey) {
90+
return requestKey;
91+
}
92+
93+
// Fall back to default key if configured
94+
if (this.config.defaultApiKey) {
95+
return this.config.defaultApiKey;
96+
}
97+
98+
// If authentication is required and no key is available, throw error
99+
if (this.config.requireAuthentication) {
100+
throw new Error("API key is required. Provide apiKey parameter or configure server default.");
101+
}
102+
103+
throw new Error("No API key available");
104+
}
105+
106+
/**
107+
* Authenticate a request and return result
108+
*/
109+
async authenticate(requestKey?: string): Promise<AuthenticationResult> {
110+
const startTime = Date.now();
111+
112+
try {
113+
const effectiveKey = await this.getEffectiveApiKey(requestKey);
114+
const usedFallback = !requestKey && !!this.config.defaultApiKey;
115+
116+
const validation = await this.validateApiKey(effectiveKey);
117+
118+
return {
119+
success: validation.isValid,
120+
keyHash: validation.keyHash,
121+
usedFallback,
122+
rateLimited: validation.rateLimitInfo?.remaining === 0 || false,
123+
authTime: Date.now() - startTime,
124+
errorMessage: validation.errorMessage,
125+
};
126+
} catch (error) {
127+
return {
128+
success: false,
129+
keyHash: "unknown",
130+
usedFallback: false,
131+
rateLimited: false,
132+
authTime: Date.now() - startTime,
133+
errorMessage: error instanceof Error ? error.message : "Authentication failed",
134+
};
135+
}
136+
}
137+
138+
/**
139+
* Sanitize API key for logging
140+
*/
141+
sanitizeApiKey(apiKey: string): string {
142+
return SecureKeyHandler.sanitizeForLogs(apiKey);
143+
}
144+
145+
/**
146+
* Check if a key is rate limited
147+
*/
148+
isRateLimited(apiKey: string): boolean {
149+
const keyHash = SecureKeyHandler.hashKey(apiKey);
150+
const result = this.rateLimiter.getStatus(keyHash);
151+
return !result.allowed;
152+
}
153+
154+
/**
155+
* Invalidate cached validation for a key
156+
*/
157+
invalidateKey(apiKey: string): void {
158+
const keyHash = SecureKeyHandler.hashKey(apiKey);
159+
this.cache.invalidate(keyHash);
160+
}
161+
162+
/**
163+
* Get cache statistics
164+
*/
165+
getCacheStats() {
166+
return this.cache.getStats();
167+
}
168+
169+
/**
170+
* Get rate limiter status for a key
171+
*/
172+
getRateLimitStatus(apiKey: string) {
173+
const keyHash = SecureKeyHandler.hashKey(apiKey);
174+
return this.rateLimiter.getStatus(keyHash);
175+
}
176+
177+
/**
178+
* Perform actual key validation
179+
* In production, this would call the Lighthouse API
180+
*/
181+
private async performKeyValidation(apiKey: string): Promise<boolean> {
182+
// Basic validation: key should be non-empty and have reasonable length
183+
// In production, this would make an API call to Lighthouse to validate the key
184+
return SecureKeyHandler.isValidFormat(apiKey);
185+
}
186+
187+
/**
188+
* Destroy the auth manager and cleanup resources
189+
*/
190+
destroy(): void {
191+
this.cache.destroy();
192+
this.rateLimiter.destroy();
193+
}
194+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* Key validation cache with LRU eviction and TTL management
3+
*/
4+
5+
import { CacheConfig, CacheEntry, ValidationResult } from "./types.js";
6+
7+
export class KeyValidationCache {
8+
private cache = new Map<string, CacheEntry>();
9+
private config: CacheConfig;
10+
private cleanupInterval?: NodeJS.Timeout;
11+
12+
constructor(config: CacheConfig) {
13+
this.config = config;
14+
15+
if (this.config.enabled && this.config.cleanupIntervalSeconds > 0) {
16+
// Cleanup expired entries periodically
17+
this.cleanupInterval = setInterval(
18+
() => this.cleanup(),
19+
config.cleanupIntervalSeconds * 1000,
20+
);
21+
}
22+
}
23+
24+
/**
25+
* Get cached validation result
26+
*/
27+
get(keyHash: string): ValidationResult | null {
28+
if (!this.config.enabled) return null;
29+
30+
const entry = this.cache.get(keyHash);
31+
if (!entry) return null;
32+
33+
// Check if expired
34+
if (Date.now() > entry.expiresAt) {
35+
this.cache.delete(keyHash);
36+
return null;
37+
}
38+
39+
// Update access time for LRU
40+
entry.lastAccessed = Date.now();
41+
return entry.result;
42+
}
43+
44+
/**
45+
* Set validation result in cache
46+
*/
47+
set(keyHash: string, result: ValidationResult): void {
48+
if (!this.config.enabled) return;
49+
50+
// Manage cache size
51+
if (this.cache.size >= this.config.maxSize) {
52+
this.evictLRU();
53+
}
54+
55+
const entry: CacheEntry = {
56+
result,
57+
expiresAt: Date.now() + this.config.ttlSeconds * 1000,
58+
lastAccessed: Date.now(),
59+
};
60+
61+
this.cache.set(keyHash, entry);
62+
}
63+
64+
/**
65+
* Invalidate a specific key
66+
*/
67+
invalidate(keyHash: string): void {
68+
this.cache.delete(keyHash);
69+
}
70+
71+
/**
72+
* Clear all cache entries
73+
*/
74+
clear(): void {
75+
this.cache.clear();
76+
}
77+
78+
/**
79+
* Get cache statistics
80+
*/
81+
getStats(): {
82+
size: number;
83+
maxSize: number;
84+
hitRate: number;
85+
} {
86+
return {
87+
size: this.cache.size,
88+
maxSize: this.config.maxSize,
89+
hitRate: 0, // Would need to track hits/misses for accurate rate
90+
};
91+
}
92+
93+
/**
94+
* Evict least recently used entry
95+
*/
96+
private evictLRU(): void {
97+
if (this.cache.size === 0) return;
98+
99+
let oldestKey = "";
100+
let oldestTime = Infinity;
101+
102+
for (const [key, entry] of this.cache.entries()) {
103+
if (entry.lastAccessed < oldestTime) {
104+
oldestTime = entry.lastAccessed;
105+
oldestKey = key;
106+
}
107+
}
108+
109+
if (oldestKey) {
110+
this.cache.delete(oldestKey);
111+
}
112+
}
113+
114+
/**
115+
* Clean up expired entries
116+
*/
117+
private cleanup(): void {
118+
const now = Date.now();
119+
const keysToDelete: string[] = [];
120+
121+
for (const [key, entry] of this.cache.entries()) {
122+
if (now > entry.expiresAt) {
123+
keysToDelete.push(key);
124+
}
125+
}
126+
127+
keysToDelete.forEach((key) => this.cache.delete(key));
128+
}
129+
130+
/**
131+
* Destroy the cache and cleanup resources
132+
*/
133+
destroy(): void {
134+
if (this.cleanupInterval) {
135+
clearInterval(this.cleanupInterval);
136+
this.cleanupInterval = undefined;
137+
}
138+
this.clear();
139+
}
140+
}

0 commit comments

Comments
 (0)