Line

You are reading the unreleased docs (next). See latest (v0.9)

ui/server/src/auth-1

Cockpit server, next. Source: n/a.

Classes#

AuthConfigError#

Defined in: ui/server/src/auth.mjs:58

Thrown for a configuration the server must refuse to start with, as opposed to a request-time failure. index.mjs prints .message and exits 1 rather than dumping a stack trace at whoever ran npm start.

Extends#

  • Error

Constructors#

Constructor

new AuthConfigError(message): AuthConfigError

Defined in: ui/server/src/auth.mjs:59

Parameters
message

any

Returns

AuthConfigError

Overrides

Error.constructor

Properties#

name

name: string

Defined in: ui/server/src/auth.mjs:61

Inherited from

Error.name

Variables#

DEFAULT_SESSION_TTL_HOURS#

const DEFAULT_SESSION_TTL_HOURS: 8 = 8

Defined in: ui/server/src/auth.mjs:35


SESSION_COOKIE#

const SESSION_COOKIE: "construct_session" = 'construct_session'

Defined in: ui/server/src/auth.mjs:33


STATE_COOKIE#

const STATE_COOKIE: "construct_oauth_state" = 'construct_oauth_state'

Defined in: ui/server/src/auth.mjs:34

Functions#

createAuth()#

createAuth(config, deps?): object

Defined in: ui/server/src/auth.mjs:420

Build the auth surface from a resolved config.

deps exists so the tests can drive the OAuth round trip without a network: fetchImpl stands in for GitHub, now for the clock and randomToken for the CSRF state. onLogout runs when a session signs out (with that session's login, or null when there is none; ), so work the session started (the target app's dev server) stops with it.

Parameters#

config

any

deps?

Returns#

allows

allows: (headers) => boolean

May this request proceed? True when auth is not required at all, or when it carries a valid session. The WebSocket upgrade uses exactly this, so the socket can never be more permissive than the REST API.

Parameters
headers

any

Returns

boolean

config

config: any

describeStartup

describeStartup: () => object[]

One-line startup summary + the warnings that matter.

Returns

object[]

handleCallback

handleCallback: (req, res) => Promise<any>

Parameters
req

any

res

any

Returns

Promise<any>

handleLogin

handleLogin: (req, res) => any

Parameters
req

any

res

any

Returns

any

handleLogout

handleLogout: (req, res) => void

Parameters
req

any

res

any

Returns

void

handleSession

handleSession: (req, res) => void

Parameters
req

any

res

any

Returns

void

handleTestLogin

handleTestLogin: (req, res) => any

The e2e escape hatch — and it is a login, not a bypass.

It does not make requests authenticate themselves and it does not disable requireSession. It mints a normal signed session cookie for one configured login, so Playwright proves the real gate by actually logging in and holding a real cookie. There is no code path anywhere in this module where "an env var is set" substitutes for "this request carries a valid signed session".

Four independent things have to be true for it to do anything:

  1. CONSTRUCT_AUTH_TEST_USER is set — otherwise this route 404s and does not exist.
  2. NODE_ENV !== 'production' — otherwise the process refused to start (resolveAuthConfig).
  3. The server is bound to loopback — otherwise, likewise.
  4. The request carries the Cockpit's own Origin — so a page in another tab cannot mint one in a developer's browser. Fail-closed: a request with no Origin at all (plain curl) is refused too.
  5. The configured login passes the same allowlist the OAuth callback applies, when an allowlist exists. Also enforced at startup; kept here so the two login paths cannot drift apart later.
Parameters
req

any

res

any

Returns

any

isAllowedLogin

isAllowedLogin: (login) => any

Parameters
login

any

Returns

any

mountRoutes

mountRoutes: (app) => any

Register the /auth/* surface. These routes are deliberately outside /api, and therefore outside the gate — you cannot log in through a door that requires you to already be logged in.

Parameters
app

any

Returns

any

required

required: any = config.required

requireSession

requireSession: (req, res, next) => any

The gate. index.mjs mounts this on /api above every route it protects — not per route, so nothing can be added later that forgets to be gated.

Parameters
req

any

res

any

next

any

Returns

any

sendError

sendError: (res, status, title, detail) => void

: the same plain error page, for the separate repository-connection callback (githubRepoApi.mjs).

Parameters
res

any

status

any

title

any

detail

any

Returns

void

sessionFromHeaders

sessionFromHeaders: (headers) => any

The verified session on a request, or null. Used by the middleware, by the WebSocket upgrade check, and by /auth/session.

Parameters
headers

any

Returns

any

testLoginEnabled

testLoginEnabled: boolean


isLoopbackHost()#

isLoopbackHost(host): boolean

Defined in: ui/server/src/auth.mjs:51

A host string this server can bind to that is not reachable from another machine. Anything else counts as exposed, and exposure without authentication is refused at startup.

Parameters#

host

any

Returns#

boolean


loadOrCreatePersistedSecret()#

loadOrCreatePersistedSecret(stateDir): any

Defined in: ui/server/src/auth.mjs:198

The secret to sign sessions with when no CONSTRUCT_SESSION_SECRET is configured: read from <stateDir>/session-secret if it exists, else generated and stored there. Two calls with the same stateDir return the same secret, so a server restart no longer signs everyone out as long as the state directory survives it.

Refuses a file that is readable or writable by anyone but its owner — exactly like ssh refuses a group/world-readable private key — because trusting it would let another account on a shared machine forge sessions. An env-provided secret is never subject to this: it always wins, and this function is not called at all when one is set.

Narrow, accepted race: two processes starting for the very first time against the same empty stateDir at the same instant can each generate and write a different secret; the last writeFileSync wins on disk, but the other process keeps the value it already generated in memory. This is no worse than today's "one random secret per process" and is expected to be hit essentially never (only ever on a state directory's first boot).

Parameters#

stateDir

any

Returns#

any


parseAllowedLogins()#

parseAllowedLogins(raw): string[]

Defined in: ui/server/src/auth.mjs:162

CONSTRUCT_ALLOWED_LOGINS — comma (or whitespace) separated GitHub logins. Compared case-insensitively, because GitHub logins are.

Parameters#

raw

any

Returns#

string[]


parseCookies()#

parseCookies(header): any

Defined in: ui/server/src/auth.mjs:73

Parse a Cookie: header into a null-prototype object. First occurrence of a name wins (a second construct_session= appended by an attacker cannot override the real one), and the result has no prototype so a cookie literally named __proto__ is inert.

Parameters#

header

any

Returns#

any


resolveAuthConfig()#

resolveAuthConfig(env?, __namedParameters?): object

Defined in: ui/server/src/auth.mjs:246

Resolve the whole auth posture from the environment, or throw AuthConfigError for a configuration the server must not run with.

The rules, in one place:

  • OAuth credentials present -> login required, always, loopback or not.
  • Bound to a non-loopback host without a way to log in -> refuse to start. This is what makes 's loopback default meaningful: you cannot expose this server and still have it be open.
  • Loopback with nothing configured -> unauthenticated, as today, with a startup warning. Requiring every developer (and the e2e suite) to register an OAuth app to run a local cockpit would break dev for a threat model ("an attacker already on your machine, as you") that a cookie does not address anyway. CONSTRUCT_AUTH=required forces the gate on regardless.
  • CONSTRUCT_AUTH=off is honoured only on loopback.

The test-login hatch (CONSTRUCT_AUTH_TEST_USER) is guarded here rather than at request time on purpose: a box configured with it in production, or exposed on a real interface, fails loudly at startup instead of silently running with a back door.

Parameters#

env?

any = process.env

__namedParameters?
clientOrigin?

string = 'http://localhost:3000'

host?

string = '127.0.0.1'

port?

number = 4000

stateDir?

string = ...

Returns#

object

allowedLogins

allowedLogins: string[]

callbackUrl

callbackUrl: string

clientId

clientId: string

clientOrigin

clientOrigin: string

clientSecret

clientSecret: string

ephemeralSecret

ephemeralSecret: boolean = !sessionSecretEnv

host

host: string

loopback

loopback: boolean

oauthConfigured

oauthConfigured: boolean

required

required: boolean

secureCookies

secureCookies: boolean = !loopback

sessionSecret

sessionSecret: any

sessionSecretFile

sessionSecretFile: any

sessionTtlMs

sessionTtlMs: number

testUser

testUser: string


serializeCookie()#

serializeCookie(name, value, __namedParameters?): string

Defined in: ui/server/src/auth.mjs:94

Serialize one Set-Cookie value. httpOnly and sameSite=Lax are the defaults rather than options a caller might forget.

Parameters#

name

any

value

any

__namedParameters?
httpOnly?

boolean = true

path?

string = '/'

sameSite?

string = 'Lax'

secure?

boolean = false

Returns#

string


sessionSecretPath()#

sessionSecretPath(stateDir): any

Defined in: ui/server/src/auth.mjs:173

<stateDir>/session-secret — where the generated secret lives when CONSTRUCT_SESSION_SECRET is not set.

Parameters#

stateDir

any

Returns#

any


signValue()#

signValue(payload, secret): string

Defined in: ui/server/src/auth.mjs:116

base64url(JSON) + "." + base64url(HMAC-SHA256). base64url contains no ., so the separator is unambiguous.

Parameters#

payload

any

secret

any

Returns#

string


verifySession()#

verifySession(token, secret, now?): any

Defined in: ui/server/src/auth.mjs:146

A session is a verified value that additionally names a GitHub login.

Parameters#

token

any

secret

any

now?

number = ...

Returns#

any


verifyValue()#

verifyValue(token, secret, now?): any

Defined in: ui/server/src/auth.mjs:124

Verify signature and expiry. Returns the payload, or null — never throws, and never distinguishes why it failed to the caller, so a probing client learns nothing from the difference.

Parameters#

token

any

secret

any

now?

number = ...

Returns#

any