141 lines
4.3 KiB
JavaScript
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,
|
|
};
|
|
}
|