Files
gt-ai-os-community/packages/utils/src/auth.ts
HackWeasel b9dfb86260 GT AI OS Community Edition v2.0.33
Security hardening release addressing CodeQL and Dependabot alerts:

- Fix stack trace exposure in error responses
- Add SSRF protection with DNS resolution checking
- Implement proper URL hostname validation (replaces substring matching)
- Add centralized path sanitization to prevent path traversal
- Fix ReDoS vulnerability in email validation regex
- Improve HTML sanitization in validation utilities
- Fix capability wildcard matching in auth utilities
- Update glob dependency to address CVE
- Add CodeQL suppression comments for verified false positives

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 17:04:45 -05:00

216 lines
5.8 KiB
TypeScript

// Authentication and Authorization Utilities
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import { JWTPayload, Capability } from '@gt2/types';
// JWT Configuration
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h';
/**
* Generate a cryptographic hash for capability verification
*/
export function generateCapabilityHash(capabilities: Capability[]): string {
const capabilityString = JSON.stringify(capabilities, Object.keys(capabilities).sort());
return crypto.createHmac('sha256', JWT_SECRET).update(capabilityString).digest('hex');
}
/**
* Verify capability hash to ensure JWT hasn't been tampered with
*/
export function verifyCapabilityHash(capabilities: Capability[], hash: string): boolean {
const expectedHash = generateCapabilityHash(capabilities);
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expectedHash));
}
/**
* Create a capability-based JWT token
*/
export function createJWT(payload: Omit<JWTPayload, 'capability_hash' | 'exp' | 'iat'>): string {
const capability_hash = generateCapabilityHash(payload.capabilities);
const fullPayload: JWTPayload = {
...payload,
capability_hash,
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60), // 24 hours
iat: Math.floor(Date.now() / 1000)
};
return jwt.sign(fullPayload, JWT_SECRET, { algorithm: 'HS256' });
}
/**
* Verify and decode a JWT token
*/
export function verifyJWT(token: string): JWTPayload | null {
try {
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
// Verify capability hash to ensure token hasn't been tampered with
if (!verifyCapabilityHash(decoded.capabilities, decoded.capability_hash)) {
throw new Error('Invalid capability hash');
}
return decoded;
} catch (error) {
return null;
}
}
/**
* Check if user has required capability
*
* Supports wildcard matching for resources:
* - "*" matches all resources
* - "documents/*" matches all resources starting with "documents/"
* - "documents/read" matches only exact resource "documents/read"
*/
export function hasCapability(
userCapabilities: Capability[],
resource: string,
action: string
): boolean {
return userCapabilities.some(cap => {
// Check if capability matches resource (support wildcards)
let resourceMatch = false;
if (cap.resource === '*') {
// Wildcard matches everything
resourceMatch = true;
} else if (cap.resource === resource) {
// Exact match
resourceMatch = true;
} else if (cap.resource.endsWith('/*')) {
// Prefix wildcard: "documents/*" matches "documents/read", "documents/write", etc.
const prefix = cap.resource.slice(0, -1); // Remove trailing "*", keep "/"
resourceMatch = resource.startsWith(prefix);
} else if (cap.resource.endsWith('*')) {
// Trailing wildcard: "documents*" matches "documents", "documents/read", etc.
const prefix = cap.resource.slice(0, -1); // Remove trailing "*"
resourceMatch = resource.startsWith(prefix);
}
// Check if capability includes required action
const actionMatch = cap.actions.includes('*') || cap.actions.includes(action);
// Check constraints if present
if (cap.constraints) {
// Check validity period
if (cap.constraints.valid_until) {
const validUntil = new Date(cap.constraints.valid_until);
if (new Date() > validUntil) {
return false;
}
}
// Additional constraint checks can be added here
}
return resourceMatch && actionMatch;
});
}
/**
* Hash password for storage
*/
export async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(12);
return bcrypt.hash(password, salt);
}
/**
* Verify password against hash
*/
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
/**
* Generate secure random token
*/
export function generateSecureToken(length: number = 32): string {
return crypto.randomBytes(length).toString('hex');
}
/**
* Create tenant-scoped capabilities
*/
export function createTenantCapabilities(
tenantDomain: string,
userType: 'tenant_admin' | 'tenant_user'
): Capability[] {
const baseResource = `tenant:${tenantDomain}`;
if (userType === 'tenant_admin') {
return [
{
resource: `${baseResource}:*`,
actions: ['read', 'write', 'admin'],
constraints: {}
},
{
resource: 'ai_resource:*',
actions: ['use'],
constraints: {
usage_limits: {
max_requests_per_hour: 1000,
max_tokens_per_request: 4000
}
}
}
];
} else {
return [
{
resource: `${baseResource}:conversations`,
actions: ['read', 'write'],
constraints: {}
},
{
resource: `${baseResource}:documents`,
actions: ['read', 'write'],
constraints: {}
},
{
resource: 'ai_resource:*',
actions: ['use'],
constraints: {
usage_limits: {
max_requests_per_hour: 100,
max_tokens_per_request: 4000
}
}
}
];
}
}
/**
* Create super admin capabilities
*/
export function createSuperAdminCapabilities(): Capability[] {
return [
{
resource: '*',
actions: ['*'],
constraints: {}
}
];
}
/**
* Extract Bearer token from Authorization header
*/
export function extractBearerToken(authHeader?: string): string | null {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}
return authHeader.substring(7);
}
/**
* Check if JWT token is expired
*/
export function isTokenExpired(token: JWTPayload): boolean {
return Date.now() >= token.exp * 1000;
}