Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
dd189e3
fix(node-ui): replace token bootstrap with dashboard sessions
Jul 3, 2026
847b160
test(node-ui): stabilize dashboard session auth tests
Jul 3, 2026
8d1013b
fix(node-ui): address dashboard session review
Jul 3, 2026
a8e7b1f
fix(node-ui): refresh stale dashboard sessions
Jul 3, 2026
264a775
Address node UI auth review follow-ups
Jul 3, 2026
a550721
Address dashboard session review follow-ups
Jul 3, 2026
70a688f
Address review round 4 dashboard session feedback
Jul 3, 2026
d5efb31
Address review round 5 and CI failure
Jul 3, 2026
1973575
Address node-ui session review and e2e failures
Jul 4, 2026
8832b9a
Address delayed PR review comments
Jul 4, 2026
03813b2
Address dashboard session review feedback
Jul 4, 2026
8f3e55c
Tighten dashboard logout session safety
Jul 4, 2026
022d17a
Fix auth-session logout CSRF e2e
Jul 4, 2026
622e8c6
Avoid SSE near-expiry reconnect loop
Jul 4, 2026
949a1af
Tighten dashboard auth policy coverage
Jul 4, 2026
e60ef12
Cover dashboard CSRF session endpoint
Jul 4, 2026
69f0df8
Honor Forwarded proto for dashboard cookies
Jul 4, 2026
087a7dc
Cover SSE session expiry closure
Jul 4, 2026
ee51a4e
Handle explicit empty auth and stale CSRF
Jul 4, 2026
a83ef23
Cover CSRF on unsafe dashboard methods
Jul 4, 2026
afb358d
Narrow stale CSRF retry handling
Jul 4, 2026
6e1a664
Trust forwarded proto only from loopback
Jul 4, 2026
90ec4f9
Cover incorrect CSRF token rejection
Jul 4, 2026
e797a82
Resolve dashboard principals at auth time
Jul 4, 2026
1ffc988
Cover UI asset and SSE review gaps
Jul 4, 2026
d8834d9
Cover explicit SSE and referer auth gaps
Jul 4, 2026
dc6ccfc
Require token for loopback dashboard sessions
Jul 4, 2026
7fd09f2
Cover dashboard cors and smoke auth gaps
Jul 4, 2026
3c1d75c
Authenticate raw mock fallback e2e spec
Jul 4, 2026
f217dae
Handle dashboard origin metadata and gate ownership
Jul 4, 2026
8dd4d81
Block cross-site dashboard session exchange
Jul 4, 2026
0a644ed
Guard dashboard session refresh races
Jul 4, 2026
955fbfe
Support cross-origin dashboard session review gaps
Jul 4, 2026
5bbb47a
Close stale dashboard SSE and code CSRF retries
Jul 4, 2026
104be8f
Cover dashboard exchange and unlock failures
Jul 4, 2026
f7ed67c
Make daemon fetch path contract explicit
Jul 4, 2026
7a6700a
Update node-ui compatibility assertion for daemon paths
Jul 4, 2026
1aec8f5
Share auth CORS header policy
Jul 4, 2026
7680931
Cover dashboard status and connect session paths
Jul 4, 2026
a9a1cea
Allow blob-backed UI previews in CSP
Jul 4, 2026
e8847c8
Allow UI font origins in CSP
Jul 4, 2026
066cd95
Add credentialed CORS to node-ui API responses
Jul 4, 2026
c47979e
Handle proxied HTTPS dashboard origins
Jul 4, 2026
d01aecd
Keep node-ui daemon paths typed
Jul 4, 2026
0da257a
Add dashboard username password login
Jul 4, 2026
a2f725b
Create dashboard credentials in setup flows
Jul 4, 2026
8f878fc
Harden dashboard credential setup review gaps
Jul 4, 2026
b73da51
Address dashboard login review gaps
Jul 4, 2026
e652ed6
Address dashboard login follow-up review
Jul 4, 2026
6793e7c
Require fingerprints for login sessions
Jul 4, 2026
58ec3ce
Tighten dashboard login review gaps
Jul 5, 2026
a5b65e3
Bound dashboard login attempts
Jul 5, 2026
40e9c3b
Cover dashboard login CSRF contract
Jul 5, 2026
965dba6
Assert password-login cookie attributes
Jul 5, 2026
1a94b4d
Harden dashboard login review gaps
Jul 5, 2026
5cad89f
Revalidate dashboard login event streams
Jul 5, 2026
160958d
Cover dashboard login setup review gaps
Jul 5, 2026
acdebb1
Harden dashboard credential verification
Jul 5, 2026
2928480
Merge pull request #1439 from OriginTrail/codex/node-ui-dashboard-login
Jurij89 Jul 5, 2026
b6f58d0
Refine dashboard login follow-up boundaries
Jul 5, 2026
55d2a64
Address dashboard follow-up review gaps
Jul 5, 2026
bc13750
Collapse dashboard SSE auth metadata
Jul 5, 2026
a9da311
Revalidate dashboard SSE sessions
Jul 5, 2026
6daa067
Cover resolved dashboard SSE comments
Jul 5, 2026
1badd52
Preserve dashboard auth compatibility
Jul 5, 2026
2b191a8
Keep fingerprints out of request auth
Jul 5, 2026
c3b387f
Reuse dashboard credential result types
Jul 5, 2026
3cab575
Cover MCP credential setup wiring
Jul 5, 2026
064cabe
Merge pull request #1451 from OriginTrail/codex/dashboard-login-follo…
Jurij89 Jul 5, 2026
9d535ea
Harden dashboard auth follow-up coverage
Jul 5, 2026
343929e
Address dashboard auth follow-up review
Jul 5, 2026
921d5aa
Tighten dashboard login limiter API
Jul 5, 2026
f7e8737
Split dashboard auth and static UI modules
Jul 5, 2026
7b77e85
Centralize dashboard principal and limiter state
Jul 5, 2026
8aef2a5
Merge pull request #1460 from OriginTrail/codex/dashboard-auth-bounda…
Jurij89 Jul 5, 2026
86455bf
Close dashboard auth boundary follow-up
Jul 5, 2026
c923a03
Address dashboard auth review comments
Jul 5, 2026
c06881a
Collapse dashboard auth context handoff
Jul 5, 2026
65bc558
Tighten dashboard auth review coverage
Jul 5, 2026
1b25ac9
Preserve plugin and sign-join contracts
Jul 6, 2026
4def731
Enforce login session fingerprint at runtime
Jul 6, 2026
fe5eadb
Merge pull request #1465 from OriginTrail/codex/dashboard-auth-1433-c…
Jurij89 Jul 6, 2026
77318af
Tighten daemon route auth identity handoff
Jul 6, 2026
4680aa1
Clarify route auth context handoff
Jul 6, 2026
e8bd4dc
Keep route dispatch identity boundary strict
Jul 6, 2026
0f74f77
Pin public plugin context surface
Jul 6, 2026
bf03d77
Cover plugin and route identity review gaps
Jul 6, 2026
4667af2
Use checked plugin type fixture
Jul 6, 2026
0e47c7e
Clarify auth resolver and plugin boundaries
Jul 6, 2026
3eeed32
Tighten plugin boundary review followups
Jul 6, 2026
c753511
Anchor plugin context compatibility keys
Jul 6, 2026
96e4115
Assert plugin context compatibility key diffs
Jul 6, 2026
a171eb1
Merge pull request #1471 from OriginTrail/codex/dashboard-auth-route-…
Jurij89 Jul 6, 2026
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
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,30 @@ jobs:
run: tar -xzf /tmp/build-outputs.tgz
- name: "node-ui tests"
run: pnpm --filter @origintrail-official/dkg-node-ui test
- name: "node-ui static bundle contract"
shell: bash
run: |
pnpm --filter @origintrail-official/dkg-node-ui build:ui
test -f packages/node-ui/dist-ui/index.html
if grep -R "__DKG_TOKEN__" packages/node-ui/dist-ui; then
echo "dist-ui must not contain legacy browser bearer-token bootstrap"
exit 1
fi
node - <<'NODE'
const { readFileSync, existsSync } = require('node:fs');
const { join, dirname } = require('node:path');
const indexPath = join('packages', 'node-ui', 'dist-ui', 'index.html');
const html = readFileSync(indexPath, 'utf8');
const refs = [...html.matchAll(/(?:src|href)="([^"]+\.(?:js|css))"/g)].map((m) => m[1]);
if (refs.length === 0) {
throw new Error('dist-ui/index.html did not reference any JS/CSS assets');
}
for (const ref of refs) {
const normalized = ref.startsWith('/ui/') ? ref.slice('/ui/'.length) : ref.replace(/^\.\//, '');
Comment thread
Jurij89 marked this conversation as resolved.
const assetPath = join(dirname(indexPath), normalized);
if (!existsSync(assetPath)) throw new Error(`Missing referenced UI asset: ${ref}`);
}
NODE

# node-ui Playwright e2e — REAL NODE, NO MOCKS. The suite boots a live
# 4-node devnet (Hardhat + four DKG daemons) via Playwright's `webServer`
Expand Down
154 changes: 138 additions & 16 deletions packages/cli/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,53 @@ import { existsSync } from 'node:fs';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { dkgDir } from './config.js';

export type RequestAuthPrincipal = {
kind: 'agent' | 'node-admin';
agentAddress: string;
};

export interface HttpAuthGuardOptions {
resolvePrincipal?: (token: string) => RequestAuthPrincipal;
authSources?: RequestAuthSource[];
}

interface RequestAuthBaseContext {
principal: RequestAuthPrincipal;
csrf: {
required: boolean;
validated: boolean;
};
}

export type RequestAuthContext =
| (RequestAuthBaseContext & {
source: 'authorization-header';
token: string;
})
| (RequestAuthBaseContext & {
source: 'events-query';
token: string;
})
| (RequestAuthBaseContext & {
source: 'dashboard-session';
internalCredentialToken: string;
dashboardSession: {
Comment thread
Jurij89 marked this conversation as resolved.
sessionId?: string;
source?: 'loopback' | 'exchange';
expiresAt?: number;
};
});

const REQUEST_AUTH_CONTEXT = Symbol('dkg.requestAuthContext');

export function setRequestAuthContext(req: IncomingMessage, context: RequestAuthContext): void {
(req as IncomingMessage & { [REQUEST_AUTH_CONTEXT]?: RequestAuthContext })[REQUEST_AUTH_CONTEXT] = context;
}

export function getRequestAuthContext(req: IncomingMessage): RequestAuthContext | undefined {
return (req as IncomingMessage & { [REQUEST_AUTH_CONTEXT]?: RequestAuthContext })[REQUEST_AUTH_CONTEXT];
}

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -275,9 +322,13 @@ function reconcileFileTokens(validTokens: Set<string>): void {
* Performs an mtime-gated hot-reload of the on-disk `auth.token` file
* on every call — see `reconcileFileTokens` above for the rationale.
*/
export function reconcileValidTokens(validTokens: Set<string>): void {
reconcileFileTokens(validTokens);
}

export function verifyToken(token: string | undefined, validTokens: Set<string>): boolean {
if (!token) return false;
reconcileFileTokens(validTokens);
reconcileValidTokens(validTokens);
return validTokens.has(token);
}

Expand Down Expand Up @@ -763,6 +814,77 @@ function isPublicPath(method: string, pathname: string): boolean {
return false;
}

export type RequestAuthDecision =
| { ok: true; context: RequestAuthContext; credentialToken: string }
| { ok: false; status: 403; error: string };

export interface RequestAuthSource {
resolve: (
req: IncomingMessage,
validTokens: Set<string>,
corsOrigin?: string | null,
) => RequestAuthDecision | null;
}

function defaultRequestPrincipal(): RequestAuthPrincipal {
return { kind: 'node-admin', agentAddress: 'unknown' };
}

function resolveRequestPrincipal(token: string, options?: HttpAuthGuardOptions): RequestAuthPrincipal {
return options?.resolvePrincipal?.(token) ?? defaultRequestPrincipal();
}

export function resolveRequestAuthDecision(
req: IncomingMessage,
validTokens: Set<string>,
options?: HttpAuthGuardOptions,
corsOrigin?: string | null,
): RequestAuthDecision | null {
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
const pathname = url.pathname;
const token = extractBearerToken(req.headers.authorization);

if (token && verifyToken(token, validTokens)) {
return {
ok: true,
credentialToken: token,
context: {
source: 'authorization-header',
token,
principal: resolveRequestPrincipal(token, options),
csrf: { required: false, validated: false },
},
};
}

const hasEventsQueryToken = pathname === '/api/events' && url.searchParams.has('token');
if (hasEventsQueryToken) {
const queryToken = url.searchParams.get('token') ?? undefined;
if (queryToken && verifyToken(queryToken, validTokens)) {
return {
ok: true,
credentialToken: queryToken,
context: {
source: 'events-query',
token: queryToken,
principal: resolveRequestPrincipal(queryToken, options),
csrf: { required: false, validated: false },
},
};
}
}

const explicitAuthAttempt = Boolean(token) || hasEventsQueryToken;
if (explicitAuthAttempt) return null;

for (const source of options?.authSources ?? []) {
const decision = source.resolve(req, validTokens, corsOrigin);
if (decision) return decision;
}

return null;
}

/**
* CLI-10 /.
*
Expand Down Expand Up @@ -827,28 +949,28 @@ export function httpAuthGuard(
authEnabled: boolean,
validTokens: Set<string>,
corsOrigin?: string | null,
options?: HttpAuthGuardOptions,
): boolean | Promise<boolean> {
if (!authEnabled) return true;
if (req.method === 'OPTIONS') return true;

const pathname = new URL(req.url ?? '/', `http://${req.headers.host}`).pathname;
if (isPublicPath(req.method ?? '', pathname)) return true;

const token = extractBearerToken(req.headers.authorization);
let acceptedToken: string | undefined;
if (verifyToken(token, validTokens)) {
acceptedToken = token;
} else if (pathname === '/api/events') {
// EventSource can't set headers — accept token as query param, but ONLY
// for the SSE endpoint to avoid leaking credentials in URLs/logs/referrers.
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
const qsToken = url.searchParams.get('token');
if (qsToken && verifyToken(qsToken, validTokens)) {
acceptedToken = qsToken;
}
const authDecision = resolveRequestAuthDecision(req, validTokens, options, corsOrigin);
if (authDecision?.ok === false) {
res.writeHead(authDecision.status, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': corsOrigin ?? '*',
});
res.end(JSON.stringify({ error: authDecision.error }));
return false;
}

if (acceptedToken) {
if (authDecision?.ok === true) {
setRequestAuthContext(req, authDecision.context);
const acceptedCredentialToken = authDecision.credentialToken;

const now = Date.now();

// CLI-10: stale-timestamp gate. If the client opted into the
Expand Down Expand Up @@ -925,7 +1047,7 @@ export function httpAuthGuard(
// pre-check and the full verifier enforce identical replay
// semantics.
pruneNonces(now);
const preBodyNonceScope = createHash('sha256').update(acceptedToken).digest('hex');
const preBodyNonceScope = createHash('sha256').update(acceptedCredentialToken).digest('hex');
const preBodyNonceKey = `${preBodyNonceScope}:${nonceHeader}`;
if (seenNonces.has(preBodyNonceKey)) {
res.writeHead(401, {
Expand All @@ -941,7 +1063,7 @@ export function httpAuthGuard(
// buffering the body. The actual HMAC check happens there
// (or synchronously below for body-less requests).
(req as unknown as { __dkgSignedAuth?: SignedAuthPending }).__dkgSignedAuth = {
token: acceptedToken,
token: acceptedCredentialToken,
timestamp: tsHeader,
nonce: nonceHeader,
signature: sigHeader,
Expand Down
Loading
Loading