Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **docs:** add management authentication terminology guide ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))
41 changes: 41 additions & 0 deletions .claude/worktrees/feat-7786/docs/guides/MANAGEMENT-AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Management Authentication

OmniRoute uses four distinct credential families for management access. This guide
distinguishes them by purpose, scope, and locality.

| Credential | Scope | Locality | Use Case |
|-------------------------|--------------------|---------------|-----------------------------------|
| Dashboard JWT session | Full management | Localhost | Web dashboard login |
| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands |
| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access |
| Manage-scope API key | `manage` scope | External | Management API calls |

## Dashboard JWT Session

Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie.
Valid for the session duration. Cannot be used from external hosts.

## CLI Machine-ID Token

Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`.
Used by the CLI for all management operations. Tied to the machine identity.

## Scoped `oma_` Access Token

Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`).
Format: `oma_<random-hex>`. Used for programmatic access from external systems.

## Manage-Scope API Key

Standard API key with the `manage` scope enabled. Created in dashboard API Keys page.
Used for management API calls from external hosts.

## Header Examples

```
Authorization: Bearer oma_abc123def456
Authorization: Bearer <standard-api-key-with-manage-scope>
Cookie: omniroute_session=<jwt-token>
```

See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, it } from "node:test";
import { ok } from "node:assert/strict";
import { readFileSync } from "node:fs";

describe("Management auth documentation (#7786)", () => {
const docPath = "docs/guides/MANAGEMENT-AUTH.md";
const content = readFileSync(docPath, "utf-8");

it("exists and has content", () => {
ok(content.length > 500, "should have substantial content");
ok(content.includes("Dashboard JWT session"));
ok(content.includes("CLI machine-id token"));
ok(content.includes("oma_"));
});

it("documents all four credential families", () => {
const families = ["Dashboard JWT", "CLI machine-id", "oma_", "Manage-scope"];
for (const f of families) {
ok(content.includes(f), `should document ${f}`);
}
});

it("mentions relevant auth header examples", () => {
ok(content.includes("Authorization"));
ok(content.includes("Bearer"));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(infra):** add systemd autostart unit for Linux ([#8635](https://github.com/diegosouzapw/OmniRoute/issues/8635))
19 changes: 19 additions & 0 deletions .claude/worktrees/feat-8635/contrib/systemd/omniroute.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[Unit]
Description=OmniRoute AI Proxy
After=network.target network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=$(which omniroute) start
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production

# Security hardening
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true

[Install]
WantedBy=default.target
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, it } from "node:test";
import { ok } from "node:assert/strict";
import { readFileSync, existsSync } from "node:fs";

describe("Systemd autostart (#8635)", () => {
const svcPath = "contrib/systemd/omniroute.service";
const content = readFileSync(svcPath, "utf-8");

it("service file exists", () => {
ok(existsSync(svcPath));
ok(content.length > 200);
});

it("defines required systemd sections", () => {
ok(content.includes("[Unit]"));
ok(content.includes("[Service]"));
ok(content.includes("[Install]"));
});

it("specifies WantedBy=default.target", () => {
ok(content.includes("WantedBy=default.target"));
});
});
1 change: 1 addition & 0 deletions changelog.d/fixes/9159-fix.plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- fix(management): authorize mcp:connect-only keys on loopback/LAN when requireLogin is enabled (#9159)
1 change: 1 addition & 0 deletions changelog.d/fixes/9491-port-3005-auth-redirect-login.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(auth):** redirect active sessions from /login by checking the session cookie before showing the login form. (thanks @DaDecky)
23 changes: 23 additions & 0 deletions src/app/api/settings/require-login/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { jwtVerify } from "jose";
import { getSettings, updateSettings } from "@/lib/localDb";
import {
hasManagementPasswordConfigured,
Expand All @@ -9,6 +11,24 @@ import { getNodeRuntimeSupport } from "@/shared/utils/nodeRuntimeSupport.ts";
import { updateRequireLoginSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";

function getJwtSecret(): Uint8Array | null {
const secret = process.env.JWT_SECRET?.trim();
return secret ? new TextEncoder().encode(secret) : null;
}

async function checkSessionAuthenticated(): Promise<boolean> {
try {
const cookieStore = await cookies();
const token = cookieStore.get("auth_token")?.value;
const secret = getJwtSecret();
if (!token || !secret) return false;
await jwtVerify(token, secret);
return true;
} catch {
return false;
}
}

// Node.js compatibility check — reflect the supported secure runtime floors used by CLI/CI.
function getNodeCompatibility() {
const { nodeVersion, nodeCompatible } = getNodeRuntimeSupport();
Expand All @@ -28,10 +48,12 @@ export async function GET() {
try {
const settings = await getSettings();
const requireLogin = settings.requireLogin !== false;
const authenticated = await checkSessionAuthenticated();
const hasPassword = hasManagementPasswordConfigured(settings);
const setupComplete = !!settings.setupComplete;
const oidcEnabled = !!settings.oidcEnabled;
return NextResponse.json({
authenticated,
requireLogin,
hasPassword,
setupComplete,
Expand All @@ -42,6 +64,7 @@ export async function GET() {
console.error("[API] Error fetching require-login settings:", error);
return NextResponse.json(
{
authenticated: false,
requireLogin: true,
hasPassword: true,
setupComplete: true,
Expand Down
2 changes: 1 addition & 1 deletion src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function LoginPage() {
const data = await res.json();
if (data.nodeVersion) setNodeVersion(data.nodeVersion);
if (data.nodeCompatible === false) setNodeCompatible(false);
if (data.requireLogin === false) {
if (data.authenticated === true || data.requireLogin === false) {
router.push("/dashboard");
router.refresh();
return;
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/auth-redirect-login.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Auth redirect: active sessions are redirected from /login to /dashboard.
*
* Upstream: decolua/9router#3005 — fix(auth): redirect active sessions from /login
* When a user navigates to /login while already authenticated, the login page
* fetches /api/settings/require-login and redirects to /dashboard.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";

describe("auth redirect login (port from 9router#3005)", () => {
it("login page checks data.authenticated === true before showing the form", () => {
const source = fs.readFileSync(path.resolve("src/app/login/page.tsx"), "utf-8");
// The redirect guard must check both:
// - authenticated=true (active session → redirect to dashboard)
// - requireLogin=false (no auth configured → allow access)
const redirectCheck = source.match(/if\s*\(.*authenticated.*requireLogin.*\)/);
assert.ok(redirectCheck, "login page must check both authenticated and requireLogin");
assert.ok(
source.includes("data.authenticated === true"),
"login page must check data.authenticated === true for redirect",
);
});

it("require-login API route returns authenticated field", () => {
const source = fs.readFileSync(
path.resolve("src/app/api/settings/require-login/route.ts"),
"utf-8",
);
assert.ok(
source.includes("authenticated:"),
"require-login route must include authenticated in the response",
);
assert.ok(
source.includes("authenticated,"),
"authenticated must be part of the JSON response object (spread or key)",
);
});
});
Loading