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
Overrides
Error.constructor
Properties#
name
name:
string
Defined in: ui/server/src/auth.mjs:61
Inherited from
Error.name
Variables#
DEFAULT_SESSION_TTL_HOURS#
constDEFAULT_SESSION_TTL_HOURS:8=8
Defined in: ui/server/src/auth.mjs:35
SESSION_COOKIE#
constSESSION_COOKIE:"construct_session"='construct_session'
Defined in: ui/server/src/auth.mjs:33
STATE_COOKIE#
constSTATE_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:
CONSTRUCT_AUTH_TEST_USERis set — otherwise this route 404s and does not exist.NODE_ENV !== 'production'— otherwise the process refused to start (resolveAuthConfig).- The server is bound to loopback — otherwise, likewise.
- 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.
- 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=requiredforces the gate on regardless. CONSTRUCT_AUTH=offis 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