Files
MQTT-Explorer/src/AuthManager.ts
T

130 lines
3.8 KiB
TypeScript

import * as fs from 'fs'
import * as path from 'path'
import * as bcrypt from 'bcryptjs'
import * as crypto from 'crypto'
import { v4 as uuidv4 } from 'uuid'
export interface Credentials {
username: string
passwordHash: string
}
export class AuthManager {
private credentialsPath: string
private credentials: Credentials | undefined
private skipAuth: boolean
constructor(credentialsPath: string) {
this.credentialsPath = credentialsPath
this.skipAuth = process.env.MQTT_EXPLORER_SKIP_AUTH === 'true'
}
public isAuthDisabled(): boolean {
return this.skipAuth
}
public async initialize(): Promise<void> {
const isProduction = process.env.NODE_ENV === 'production'
// Check if authentication is disabled
if (this.skipAuth) {
console.log('='.repeat(60))
console.log('WARNING: Authentication is DISABLED')
console.log('MQTT_EXPLORER_SKIP_AUTH=true')
console.log('This should only be used behind a secure authentication proxy!')
console.log('='.repeat(60))
return
}
// Try to get credentials from environment variables
const envUsername = process.env.MQTT_EXPLORER_USERNAME
const envPassword = process.env.MQTT_EXPLORER_PASSWORD
if (envUsername && envPassword) {
// Use environment credentials
if (!isProduction) {
console.log('Using credentials from environment variables')
console.log('Username:', envUsername)
}
this.credentials = {
username: envUsername,
passwordHash: await bcrypt.hash(envPassword, 10),
}
return
}
// Try to load from file
if (fs.existsSync(this.credentialsPath)) {
try {
const data = fs.readFileSync(this.credentialsPath, 'utf8')
this.credentials = JSON.parse(data)
if (!isProduction && this.credentials) {
console.log('Loaded credentials from', this.credentialsPath)
console.log('Username:', this.credentials.username)
}
return
} catch (error) {
console.error('Failed to load credentials from file:', error)
}
}
// Generate new credentials
const username = `user-${uuidv4().substring(0, 8)}`
const password = uuidv4()
console.log('='.repeat(60))
console.log('Generated new credentials:')
console.log('Username:', username)
console.log('Password:', password)
console.log('='.repeat(60))
console.log('Please save these credentials. They will be persisted to:')
console.log(this.credentialsPath)
console.log('='.repeat(60))
console.log('IMPORTANT: In production, use environment variables:')
console.log('export MQTT_EXPLORER_USERNAME=<username>')
console.log('export MQTT_EXPLORER_PASSWORD=<password>')
console.log('='.repeat(60))
this.credentials = {
username,
passwordHash: await bcrypt.hash(password, 10),
}
// Save to file
try {
const dir = path.dirname(this.credentialsPath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(this.credentialsPath, JSON.stringify(this.credentials, null, 2))
console.log('Credentials saved successfully')
} catch (error) {
console.error('Failed to save credentials:', error)
}
}
public async verifyCredentials(username: string, password: string): Promise<boolean> {
if (!this.credentials) {
return false
}
// Use constant-time comparison for username to prevent timing attacks
const usernameMatch = crypto.timingSafeEqual(
Buffer.from(username.padEnd(256, '\0')),
Buffer.from(this.credentials.username.padEnd(256, '\0'))
)
if (!usernameMatch) {
return false
}
return bcrypt.compare(password, this.credentials.passwordHash)
}
public getUsername(): string | undefined {
return this.credentials?.username
}
}