verae-middleware/src/services/authService.js

141 lines
4.3 KiB
JavaScript

/**
* @fileoverview Auth bridge: API keys / sessions → Verae JWT context.
* @module services/authService
*/
import { veraeClient } from '../clients/veraeClient.js';
import { getTenantByApiKey, getTenant } from '../store/tenants.js';
import { issueSessionToken, parseSessionToken, isApiKey } from '../lib/tokens.js';
import { AppError } from '../errors.js';
import { createDebugger } from '../debug/logger.js';
const log = createDebugger('auth');
/**
* Login with Verae username/password and issue middleware session token.
*
* @param {object} params
* @param {string} params.username
* @param {string} params.password
* @param {object} [params.tenant]
* @returns {Promise<{ accessToken: string, expiresAt: string, tenant: object, user: object }>}
*/
export async function loginWithCredentials({ username, password, tenant }) {
log.debug('loginWithCredentials', { username, tenantId: tenant?.id });
const verae = await veraeClient.login({ username, password });
if (tenant?.veraeUsername && tenant.veraeUsername !== username) {
throw new AppError('Credentials do not match tenant account', {
status: 403,
code: 'FORBIDDEN',
});
}
const tenantId = tenant?.id ?? `user:${verae.user.username}`;
const accessToken = issueSessionToken({
tenantId,
veraeToken: verae.token,
expiresAt: verae.expiresAt,
});
return {
accessToken,
expiresAt: verae.expiresAt,
tenant: tenant
? { id: tenant.id, name: tenant.name, plan: tenant.plan }
: { id: tenantId, name: verae.user.username, plan: 'free' },
user: verae.user,
};
}
/**
* Resolve API key to tenant and login with stored Verae credentials.
* @param {string} apiKey
* @returns {Promise<Awaited<ReturnType<typeof loginWithCredentials>>>}
*/
export async function loginWithApiKey(apiKey) {
const tenant = getTenantByApiKey(apiKey);
if (!tenant) {
log.debug('invalid api key');
throw new AppError('Invalid API key', { status: 401, code: 'UNAUTHORIZED' });
}
if (!tenant.veraeUsername || !tenant.veraePassword) {
throw new AppError('Tenant is missing Verae credentials', {
status: 500,
code: 'TENANT_MISCONFIGURED',
});
}
log.debug('loginWithApiKey', { tenantId: tenant.id, plan: tenant.plan });
return loginWithCredentials({
username: tenant.veraeUsername,
password: tenant.veraePassword,
tenant,
});
}
/**
* Resolve bearer credential into request auth context.
*
* @param {string|null} rawToken
* @returns {Promise<{ tenantId: string, tenant: object, veraeToken: string, authMethod: string }>}
*/
export async function resolveAuthContext(rawToken) {
if (!rawToken) {
throw new AppError('Missing authorization token', { status: 401, code: 'UNAUTHORIZED' });
}
if (isApiKey(rawToken)) {
const session = await loginWithApiKey(rawToken);
const parsed = parseSessionToken(session.accessToken);
const tenant = getTenant(session.tenant.id) ?? session.tenant;
return {
tenantId: session.tenant.id,
tenant: { id: session.tenant.id, name: session.tenant.name, plan: session.tenant.plan },
veraeToken: parsed.veraeToken,
authMethod: 'api_key',
fullTenant: tenant,
};
}
const parsed = parseSessionToken(rawToken);
if (!parsed?.veraeToken) {
throw new AppError('Invalid or expired session token', { status: 401, code: 'UNAUTHORIZED' });
}
if (parsed.expiresAt && Date.parse(parsed.expiresAt) < Date.now()) {
throw new AppError('Session token expired', { status: 401, code: 'TOKEN_EXPIRED' });
}
const tenant = getTenant(parsed.tenantId);
log.debug('session auth', { tenantId: parsed.tenantId });
return {
tenantId: parsed.tenantId,
tenant: tenant
? { id: tenant.id, name: tenant.name, plan: tenant.plan }
: { id: parsed.tenantId, plan: 'free' },
veraeToken: parsed.veraeToken,
authMethod: 'session',
fullTenant: tenant,
};
}
/**
* Validate auth and optionally ping Verae /auth/validate.
* @param {string} rawToken
* @returns {Promise<object>}
*/
export async function validateSession(rawToken) {
const context = await resolveAuthContext(rawToken);
const validation = await veraeClient.validate(context.veraeToken);
return {
valid: true,
tenantId: context.tenantId,
plan: context.tenant.plan,
authMethod: context.authMethod,
user: validation,
};
}