Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .trajectories/active/traj_7ludwvz45veh.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,42 @@
"reasoning": "Hamburger menu visibility, logs button always visible on mobile, responsive padding throughout"
},
"significance": "high"
},
{
"ts": 1767636873294,
"type": "decision",
"content": "Starting work on bd-critical-016: Workspace Daemon Auth security fix: Starting work on bd-critical-016: Workspace Daemon Auth security fix",
"raw": {
"question": "Starting work on bd-critical-016: Workspace Daemon Auth security fix",
"chosen": "Starting work on bd-critical-016: Workspace Daemon Auth security fix",
"alternatives": [],
"reasoning": ""
},
"significance": "high"
},
{
"ts": 1767637700734,
"type": "decision",
"content": "Implemented workspace token auth for CLI endpoints: Implemented workspace token auth for CLI endpoints",
"raw": {
"question": "Implemented workspace token auth for CLI endpoints",
"chosen": "Implemented workspace token auth for CLI endpoints",
"alternatives": [],
"reasoning": "Added HMAC-based token validation to prevent unauthorized access to auth sessions"
},
"significance": "high"
},
{
"ts": 1767637829693,
"type": "decision",
"content": "bd-critical-016: Chose HMAC-SHA256 token validation over simpler approaches: bd-critical-016: Chose HMAC-SHA256 token validation over simpler approaches",
"raw": {
"question": "bd-critical-016: Chose HMAC-SHA256 token validation over simpler approaches",
"chosen": "bd-critical-016: Chose HMAC-SHA256 token validation over simpler approaches",
"alternatives": [],
"reasoning": "Matches existing provisioner token generation, cryptographically secure, no additional dependencies needed"
},
"significance": "high"
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion .trajectories/index.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"lastUpdated": "2026-01-05T17:59:19.223Z",
"lastUpdated": "2026-01-05T18:30:29.700Z",
"trajectories": {
"traj_ozd98si6a7ns": {
"title": "Fix thinking indicator showing on all messages",
Expand Down
60 changes: 50 additions & 10 deletions src/cloud/api/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as crypto from 'crypto';
import { requireAuth } from './auth.js';
import { db } from '../db/index.js';
import { vault } from '../vault/index.js';
import { getConfig } from '../config.js';

// Import for local use
import {
Expand Down Expand Up @@ -51,6 +52,25 @@ export {

export const onboardingRouter = Router();

/**
* Generate workspace token for authenticating requests to workspace daemon.
* Must match the token generation in provisioner/index.ts
*/
function generateWorkspaceToken(workspaceId: string): string {
const config = getConfig();
return crypto
.createHmac('sha256', config.sessionSecret)
.update(`workspace:${workspaceId}`)
.digest('hex');
}

/**
* Get Authorization header for workspace daemon requests
*/
function getWorkspaceAuthHeader(workspaceId: string): string {
return `Bearer ${generateWorkspaceToken(workspaceId)}`;
}

// Debug: log all requests to this router
onboardingRouter.use((req, res, next) => {
console.log(`[onboarding] ${req.method} ${req.path} - body:`, JSON.stringify(req.body));
Expand Down Expand Up @@ -80,6 +100,7 @@ interface CLIAuthSession {
// Workspace delegation fields (set when auth runs in workspace daemon)
workspaceUrl?: string;
workspaceSessionId?: string;
workspaceId?: string; // For generating auth token
}

const activeSessions = new Map<string, CLIAuthSession>();
Expand Down Expand Up @@ -185,7 +206,10 @@ onboardingRouter.post('/cli/:provider/start', async (req: Request, res: Response

const authResponse = await fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Authorization': getWorkspaceAuthHeader(workspace.id),
},
body: JSON.stringify({ useDeviceFlow }),
});

Expand Down Expand Up @@ -217,6 +241,7 @@ onboardingRouter.post('/cli/:provider/start', async (req: Request, res: Response
// Store workspace info for status polling and auth code forwarding
workspaceUrl,
workspaceSessionId: workspaceSession.sessionId,
workspaceId: workspace.id,
};

activeSessions.set(sessionId, session);
Expand Down Expand Up @@ -253,10 +278,13 @@ onboardingRouter.get('/cli/:provider/status/:sessionId', async (req: Request, re
}

// If we have workspace info, poll the workspace for status
if (session.workspaceUrl && session.workspaceSessionId) {
if (session.workspaceUrl && session.workspaceSessionId && session.workspaceId) {
try {
const statusResponse = await fetch(
`${session.workspaceUrl}/auth/cli/${provider}/status/${session.workspaceSessionId}`
`${session.workspaceUrl}/auth/cli/${provider}/status/${session.workspaceSessionId}`,
{
headers: { 'Authorization': getWorkspaceAuthHeader(session.workspaceId) },
}
);
if (statusResponse.ok) {
const workspaceStatus = await statusResponse.json() as {
Expand Down Expand Up @@ -311,7 +339,7 @@ onboardingRouter.post('/cli/:provider/complete/:sessionId', async (req: Request,
let tokenExpiresAt = session.tokenExpiresAt;

// If using workspace delegation, forward complete request first
if (session.workspaceUrl && session.workspaceSessionId) {
if (session.workspaceUrl && session.workspaceSessionId && session.workspaceId) {
// Forward authCode to workspace if provided (for Codex-style redirects)
if (authCode) {
const backendProviderId = provider === 'anthropic' ? 'anthropic' : provider;
Expand All @@ -320,7 +348,10 @@ onboardingRouter.post('/cli/:provider/complete/:sessionId', async (req: Request,

const completeResponse = await fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Authorization': getWorkspaceAuthHeader(session.workspaceId),
},
body: JSON.stringify({ authCode }),
});

Expand All @@ -337,7 +368,10 @@ onboardingRouter.post('/cli/:provider/complete/:sessionId', async (req: Request,
if (!accessToken) {
try {
const credsResponse = await fetch(
`${session.workspaceUrl}/auth/cli/${provider}/creds/${session.workspaceSessionId}`
`${session.workspaceUrl}/auth/cli/${provider}/creds/${session.workspaceSessionId}`,
{
headers: { 'Authorization': getWorkspaceAuthHeader(session.workspaceId) },
}
);
if (credsResponse.ok) {
const creds = await credsResponse.json() as {
Expand Down Expand Up @@ -424,14 +458,17 @@ onboardingRouter.post('/cli/:provider/code/:sessionId', async (req: Request, res
});

// Forward to workspace daemon
if (session.workspaceUrl && session.workspaceSessionId) {
if (session.workspaceUrl && session.workspaceSessionId && session.workspaceId) {
try {
const targetUrl = `${session.workspaceUrl}/auth/cli/${provider}/code/${session.workspaceSessionId}`;
console.log('[onboarding] Forwarding auth code to workspace:', targetUrl);

const codeResponse = await fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Authorization': getWorkspaceAuthHeader(session.workspaceId),
},
body: JSON.stringify({ code }),
});

Expand Down Expand Up @@ -485,11 +522,14 @@ onboardingRouter.post('/cli/:provider/cancel/:sessionId', async (req: Request, r
const session = activeSessions.get(sessionId);
if (session?.userId === userId) {
// Cancel on workspace side if applicable
if (session.workspaceUrl && session.workspaceSessionId) {
if (session.workspaceUrl && session.workspaceSessionId && session.workspaceId) {
try {
await fetch(
`${session.workspaceUrl}/auth/cli/${provider}/cancel/${session.workspaceSessionId}`,
{ method: 'POST' }
{
method: 'POST',
headers: { 'Authorization': getWorkspaceAuthHeader(session.workspaceId) },
}
);
} catch {
// Ignore cancel errors
Expand Down
29 changes: 29 additions & 0 deletions src/dashboard-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2110,6 +2110,35 @@ export async function startDashboard(

// ===== CLI Auth API (for workspace-based provider authentication) =====

/**
* Middleware to validate workspace token for internal endpoints.
* In cloud mode, requests to /auth/cli/* must include a valid WORKSPACE_TOKEN
* to prevent unauthorized access to auth sessions.
*/
const validateWorkspaceToken: express.RequestHandler = (req, res, next) => {
// Skip auth validation in local mode (no WORKSPACE_TOKEN set)
const expectedToken = process.env.WORKSPACE_TOKEN;
if (!expectedToken) {
return next();
}

// Extract token from Authorization header
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.substring(7)
: null;

if (!token || token !== expectedToken) {
console.warn('[dashboard] Unauthorized CLI auth request - invalid or missing workspace token');
return res.status(401).json({ error: 'Unauthorized - invalid workspace token' });
}

next();
};

// Apply workspace token validation to all CLI auth endpoints
app.use('/auth/cli', validateWorkspaceToken);

/**
* POST /auth/cli/:provider/start - Start CLI auth flow
* Body: { useDeviceFlow?: boolean }
Expand Down
Loading