From 30503add7594c7b1c5674ac12bfa270259ae75de Mon Sep 17 00:00:00 2001 From: Bradley Taylor Date: Sat, 21 Feb 2026 10:29:46 -0800 Subject: [PATCH 1/2] feat: add internal provisioning endpoint for Alpha Agent cross-provisioning Add POST /api/v1/internal/provision-apikey endpoint that allows Alpha Agent to provision AnswerAI API keys for users via Auth0 M2M tokens. The endpoint reuses existing user/org/workspace creation pipeline via verifyAAIToken middleware and returns a plaintext API key for storage in the user's container. Co-Authored-By: Claude Opus 4.6 --- .../src/aai/routes/internal-provision.ts | 107 ++++++++++++++++++ packages/server/src/index.ts | 5 +- packages/server/src/routes/index.ts | 3 +- packages/server/src/utils/constants.ts | 1 + 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/aai/routes/internal-provision.ts diff --git a/packages/server/src/aai/routes/internal-provision.ts b/packages/server/src/aai/routes/internal-provision.ts new file mode 100644 index 00000000000..9c951a5ad01 --- /dev/null +++ b/packages/server/src/aai/routes/internal-provision.ts @@ -0,0 +1,107 @@ +/* eslint-disable no-console */ +/** + * Internal Provisioning Route + * + * POST /api/v1/internal/provision-apikey + * + * Called by Alpha Agent's management Lambda (via M2M token) to provision + * an AnswerAI API key for a user. Creates user, org, workspaces, and + * API key in one shot. + * + * Authentication: Auth0 M2M JWT validated by verifyAAIToken (same RS256 flow). + * The endpoint is whitelisted so the main auth middleware is bypassed, + * and verifyAAIToken handles JWT validation + user sync directly. + */ +import express, { Request, Response } from 'express' +import { DataSource } from 'typeorm' +import { verifyAAIToken } from '../../middlewares/authentication/verifyAAIToken' +import { generateAPIKey, generateSecretHash } from '../../utils/apiKey' +import { ApiKey } from '../../database/entities/ApiKey' +import { v4 as uuidv4 } from 'uuid' + +export const createInternalProvisionRouter = (AppDataSource: DataSource) => { + const router = express.Router() + + /** + * POST /api/v1/internal/provision-apikey + * + * Body: { auth0Id, email, name, orgId } + * + * verifyAAIToken handles: + * - JWT validation (Auth0 RS256) + * - org_id validation against AUTH0_ORGANIZATION_ID allowlist + * - findOrCreateUser + findOrCreateOrganization + * - findOrCreateWorkspacesForUser + populateWorkspaceData + * - Sets req.user with full workspace context + * + * This endpoint then: + * 1. Checks for existing "AlphaAgent" API key in the workspace + * 2. Creates a new API key (since hashed keys can't be recovered) + * 3. Returns the plaintext key (only time it's visible) + */ + router.post('/provision-apikey', verifyAAIToken(AppDataSource), async (req: Request, res: Response) => { + console.log('[internal/provision-apikey] Request received') + + try { + const user = req.user as any + if (!user) { + console.log('[internal/provision-apikey] No user on request after verifyAAIToken') + return res.status(401).json({ error: 'Unauthorized' }) + } + + console.log('[internal/provision-apikey] User:', user.email, 'workspace:', user.activeWorkspaceId) + + const workspaceId = user.activeWorkspaceId + const organizationId = user.activeOrganizationId || user.organizationId + + if (!workspaceId) { + console.error('[internal/provision-apikey] No activeWorkspaceId for user', user.id) + return res.status(500).json({ error: 'User has no active workspace' }) + } + + // Delete any existing "AlphaAgent" key in this workspace + // (old keys can't be recovered since apiSecret is hashed) + const apiKeyRepo = AppDataSource.getRepository(ApiKey) + const existingKeys = await apiKeyRepo.find({ + where: { keyName: 'AlphaAgent', workspaceId } + }) + if (existingKeys.length > 0) { + console.log(`[internal/provision-apikey] Removing ${existingKeys.length} existing AlphaAgent key(s)`) + for (const key of existingKeys) { + await apiKeyRepo.delete({ id: key.id }) + } + } + + // Generate new API key + const apiKey = generateAPIKey() + const apiSecret = generateSecretHash(apiKey) + + const newKey = new ApiKey() + newKey.id = uuidv4() + newKey.apiKey = apiKey + newKey.apiSecret = apiSecret + newKey.keyName = 'AlphaAgent' + newKey.workspaceId = workspaceId + newKey.organizationId = organizationId + newKey.userId = user.id + + const keyEntity = apiKeyRepo.create(newKey) + await apiKeyRepo.save(keyEntity) + + console.log('[internal/provision-apikey] Created API key for user', user.email) + + return res.json({ + apiKey, + userId: user.id, + workspaceId + }) + } catch (error: any) { + console.error('[internal/provision-apikey] Error:', error.message) + return res.status(500).json({ error: 'Internal Server Error' }) + } + }) + + return router +} + +export default createInternalProvisionRouter diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 5580e5adda6..6189dbae483 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -14,7 +14,7 @@ import { AbortControllerPool } from './AbortControllerPool' import { RateLimiterManager } from './utils/rateLimit' import { getAllowedIframeOrigins, getCorsOptions, sanitizeMiddleware } from './utils/XSS' import { Telemetry } from './utils/telemetry' -import flowiseApiV1Router, { createAuth0Router, createAuthMeRouter } from './routes' +import flowiseApiV1Router, { createAuth0Router, createAuthMeRouter, createInternalProvisionRouter } from './routes' import errorHandlerMiddleware from './middlewares/errors' import { initCronJobs } from './utils/cron' import { WHITELIST_URLS } from './utils/constants' @@ -420,6 +420,9 @@ export class App { // AAI Auth Me route - provides enriched user data for session management this.app.use('/api/v1/auth', createAuthMeRouter(this.AppDataSource)) + // Internal provisioning route - called by Alpha Agent M2M to provision API keys + this.app.use('/api/v1/internal', createInternalProvisionRouter(this.AppDataSource)) + // ---------------------------------------- // Configure number of proxies in Host Environment // ---------------------------------------- diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 3e8dbe7aa5a..18561344af7 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -88,6 +88,7 @@ import pricingRouter from './pricing' import { createAuth0Router } from '../aai/routes/auth0' import { createAuthMeRouter } from '../aai/routes/auth-me' import { createCredentialsRefreshRouter } from '../aai/routes/credentials-refresh' +import { createInternalProvisionRouter } from '../aai/routes/internal-provision' import organizationsRouter from './organizations' @@ -188,4 +189,4 @@ router.use('/organizations', organizationsRouter) export default router // Export router factories for use in index.ts where AppDataSource is available -export { createAuth0Router, createAuthMeRouter, createCredentialsRefreshRouter } +export { createAuth0Router, createAuthMeRouter, createCredentialsRefreshRouter, createInternalProvisionRouter } diff --git a/packages/server/src/utils/constants.ts b/packages/server/src/utils/constants.ts index 2ab09990091..5a5306aa1de 100644 --- a/packages/server/src/utils/constants.ts +++ b/packages/server/src/utils/constants.ts @@ -26,6 +26,7 @@ export const WHITELIST_URLS = [ '/api/v1/metrics', '/api/v1/nvidia-nim', '/api/v1/auth/me', + '/api/v1/internal/provision-apikey', '/api/v1/auth/resolve', '/api/v1/auth/login', '/api/v1/auth/refreshToken', From d6d7faa41313191f43e20b6777bfa8d0344e7490 Mon Sep 17 00:00:00 2001 From: Bradley Taylor Date: Sat, 21 Feb 2026 10:48:06 -0800 Subject: [PATCH 2/2] fix: handle M2M tokens in provisioning endpoint M2M tokens from client_credentials grant don't carry email/org_id claims, so verifyAAIToken would 401. Switch to direct jwtCheck for signature-only validation, then read user details from the request body instead of JWT claims. Run the user/org/workspace creation pipeline inline. Co-Authored-By: Claude Opus 4.6 --- .../src/aai/routes/internal-provision.ts | 94 ++++++++++++++----- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/packages/server/src/aai/routes/internal-provision.ts b/packages/server/src/aai/routes/internal-provision.ts index 9c951a5ad01..1a4d00835ad 100644 --- a/packages/server/src/aai/routes/internal-provision.ts +++ b/packages/server/src/aai/routes/internal-provision.ts @@ -8,17 +8,31 @@ * an AnswerAI API key for a user. Creates user, org, workspaces, and * API key in one shot. * - * Authentication: Auth0 M2M JWT validated by verifyAAIToken (same RS256 flow). - * The endpoint is whitelisted so the main auth middleware is bypassed, - * and verifyAAIToken handles JWT validation + user sync directly. + * Authentication: Auth0 RS256 JWT signature validation only (M2M tokens + * don't carry user claims like email/org_id). User details come from + * the request body. */ import express, { Request, Response } from 'express' import { DataSource } from 'typeorm' -import { verifyAAIToken } from '../../middlewares/authentication/verifyAAIToken' +import { auth } from 'express-oauth2-jwt-bearer' +import { findOrCreateUser, updateUserOrganization } from '../../middlewares/authentication/findOrCreateUser' +import { findOrCreateOrganization } from '../../middlewares/authentication/findOrCreateOrganization' +import { findOrCreateWorkspacesForUser } from '../../middlewares/authentication/findOrCreateWorkspacesForUser' +import { populateWorkspaceData } from '../../middlewares/authentication/populateWorkspaceData' +import { Organization } from '../../database/entities/Organization' import { generateAPIKey, generateSecretHash } from '../../utils/apiKey' import { ApiKey } from '../../database/entities/ApiKey' import { v4 as uuidv4 } from 'uuid' +// Auth0 RS256 JWT checker — validates signature only, no user claims required. +// M2M tokens have `sub` (client ID) and `aud` but not `email`/`org_id`. +const jwtCheck = auth({ + authRequired: true, + audience: process.env.AUTH0_AUDIENCE, + issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL, + tokenSigningAlg: process.env.AUTH0_TOKEN_SIGN_ALG ?? 'RS256' +}) + export const createInternalProvisionRouter = (AppDataSource: DataSource) => { const router = express.Router() @@ -27,38 +41,70 @@ export const createInternalProvisionRouter = (AppDataSource: DataSource) => { * * Body: { auth0Id, email, name, orgId } * - * verifyAAIToken handles: - * - JWT validation (Auth0 RS256) - * - org_id validation against AUTH0_ORGANIZATION_ID allowlist - * - findOrCreateUser + findOrCreateOrganization - * - findOrCreateWorkspacesForUser + populateWorkspaceData - * - Sets req.user with full workspace context - * - * This endpoint then: - * 1. Checks for existing "AlphaAgent" API key in the workspace - * 2. Creates a new API key (since hashed keys can't be recovered) - * 3. Returns the plaintext key (only time it's visible) + * Flow: + * 1. Validate Auth0 JWT signature (M2M or user token) + * 2. Validate orgId from request body against AUTH0_ORGANIZATION_ID allowlist + * 3. findOrCreateOrganization + findOrCreateUser + workspaces + * 4. Create a new "AlphaAgent" API key (deletes existing one first) + * 5. Return plaintext key (only time it's visible) */ - router.post('/provision-apikey', verifyAAIToken(AppDataSource), async (req: Request, res: Response) => { + router.post('/provision-apikey', (req, res, next) => { + jwtCheck(req, res, (err?: any) => { + if (err) { + console.warn('[internal/provision-apikey] JWT validation failed:', err.message) + return res.status(401).json({ error: 'Unauthorized: Invalid token' }) + } + next() + }) + }, async (req: Request, res: Response) => { console.log('[internal/provision-apikey] Request received') try { - const user = req.user as any - if (!user) { - console.log('[internal/provision-apikey] No user on request after verifyAAIToken') - return res.status(401).json({ error: 'Unauthorized' }) + // Extract user details from request body (M2M tokens don't carry these) + const { auth0Id, email, name, orgId } = req.body || {} + + if (!auth0Id || !email || !orgId) { + return res.status(400).json({ + error: 'Missing required fields: auth0Id, email, orgId' + }) + } + + // Validate orgId against allowlist + const allowedOrgs = process.env.AUTH0_ORGANIZATION_ID?.split(',') || [] + if (!allowedOrgs.includes(orgId)) { + console.warn(`[internal/provision-apikey] org '${orgId}' not in allowlist`) + return res.status(401).json({ error: 'Unauthorized: Invalid organization' }) } - console.log('[internal/provision-apikey] User:', user.email, 'workspace:', user.activeWorkspaceId) + // Find or create org + const orgRepo = AppDataSource.getRepository(Organization) + let org = await orgRepo.findOneBy({ auth0Id: orgId }) + let user + + if (org) { + user = await findOrCreateUser(AppDataSource, auth0Id, email, name || email, org.id) + } else { + user = await findOrCreateUser(AppDataSource, auth0Id, email, name || email) + org = await findOrCreateOrganization(AppDataSource, orgId, name || email, user.id) + await updateUserOrganization(AppDataSource, user.id, org.id) + user.organizationId = org.id + } - const workspaceId = user.activeWorkspaceId - const organizationId = user.activeOrganizationId || user.organizationId + // Ensure workspaces exist + await findOrCreateWorkspacesForUser(AppDataSource, user, org.id) + + // Get active workspace + const workspaceData = await populateWorkspaceData(AppDataSource, user, org.id) + const workspaceId = workspaceData.activeWorkspaceId + const organizationId = workspaceData.activeOrganizationId || org.id if (!workspaceId) { console.error('[internal/provision-apikey] No activeWorkspaceId for user', user.id) return res.status(500).json({ error: 'User has no active workspace' }) } + console.log('[internal/provision-apikey] User:', email, 'workspace:', workspaceId) + // Delete any existing "AlphaAgent" key in this workspace // (old keys can't be recovered since apiSecret is hashed) const apiKeyRepo = AppDataSource.getRepository(ApiKey) @@ -88,7 +134,7 @@ export const createInternalProvisionRouter = (AppDataSource: DataSource) => { const keyEntity = apiKeyRepo.create(newKey) await apiKeyRepo.save(keyEntity) - console.log('[internal/provision-apikey] Created API key for user', user.email) + console.log('[internal/provision-apikey] Created API key for user', email) return res.json({ apiKey,