Skip to content

Commit 1dd7e4b

Browse files
authored
Add hashedPassword config (#2409)
Resolve #2225.
1 parent ff1da17 commit 1dd7e4b

File tree

6 files changed

+60
-6
lines changed

6 files changed

+60
-6
lines changed

doc/guide.md

+3
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ and then restart `code-server` with:
297297
sudo systemctl restart code-server@$USER
298298
```
299299

300+
Alternatively, you can specify the SHA-256 of your password at the `hashedPassword` field in the config file.
301+
The `hashedPassword` field takes precedence over `password`.
302+
300303
### How do I securely access development web services?
301304

302305
If you're working on a web service and want to access it locally, `code-server` can proxy it for you.

src/node/cli.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface Args extends VsArgs {
2929
config?: string
3030
auth?: AuthType
3131
password?: string
32+
hashedPassword?: string
3233
cert?: OptionalString
3334
"cert-host"?: string
3435
"cert-key"?: string
@@ -104,6 +105,12 @@ const options: Options<Required<Args>> = {
104105
type: "string",
105106
description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
106107
},
108+
hashedPassword: {
109+
type: "string",
110+
description:
111+
"The password hashed with SHA-256 for password authentication (can only be passed in via $HASHED_PASSWORD or the config file). \n" +
112+
"Takes precedence over 'password'.",
113+
},
107114
cert: {
108115
type: OptionalString,
109116
path: true,
@@ -279,6 +286,10 @@ export const parse = (
279286
throw new Error("--password can only be set in the config file or passed in via $PASSWORD")
280287
}
281288

289+
if (key === "hashedPassword" && !opts?.configFile) {
290+
throw new Error("--hashedPassword can only be set in the config file or passed in via $HASHED_PASSWORD")
291+
}
292+
282293
const option = options[key]
283294
if (option.type === "boolean") {
284295
;(args[key] as boolean) = true
@@ -361,6 +372,7 @@ export interface DefaultedArgs extends ConfigArgs {
361372
"proxy-domain": string[]
362373
verbose: boolean
363374
usingEnvPassword: boolean
375+
usingEnvHashedPassword: boolean
364376
"extensions-dir": string
365377
"user-data-dir": string
366378
}
@@ -448,13 +460,20 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi
448460
args["cert-key"] = certKey
449461
}
450462

451-
const usingEnvPassword = !!process.env.PASSWORD
463+
let usingEnvPassword = !!process.env.PASSWORD
452464
if (process.env.PASSWORD) {
453465
args.password = process.env.PASSWORD
454466
}
455467

456-
// Ensure it's not readable by child processes.
468+
const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD
469+
if (process.env.HASHED_PASSWORD) {
470+
args.hashedPassword = process.env.HASHED_PASSWORD
471+
usingEnvPassword = false
472+
}
473+
474+
// Ensure they're not readable by child processes.
457475
delete process.env.PASSWORD
476+
delete process.env.HASHED_PASSWORD
458477

459478
// Filter duplicate proxy domains and remove any leading `*.`.
460479
const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, "")))
@@ -463,6 +482,7 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi
463482
return {
464483
...args,
465484
usingEnvPassword,
485+
usingEnvHashedPassword,
466486
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
467487
}
468488

src/node/entry.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,10 @@ const main = async (args: DefaultedArgs): Promise<void> => {
9999
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
100100
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
101101

102-
if (args.auth === AuthType.Password && !args.password) {
103-
throw new Error("Please pass in a password via the config file or $PASSWORD")
102+
if (args.auth === AuthType.Password && !args.password && !args.hashedPassword) {
103+
throw new Error(
104+
"Please pass in a password via the config file or environment variable ($PASSWORD or $HASHED_PASSWORD)",
105+
)
104106
}
105107

106108
const [app, wsApp, server] = await createApp(args)
@@ -114,6 +116,8 @@ const main = async (args: DefaultedArgs): Promise<void> => {
114116
logger.info(" - Authentication is enabled")
115117
if (args.usingEnvPassword) {
116118
logger.info(" - Using password from $PASSWORD")
119+
} else if (args.usingEnvHashedPassword) {
120+
logger.info(" - Using password from $HASHED_PASSWORD")
117121
} else {
118122
logger.info(` - Using password from ${humanPath(args.config)}`)
119123
}

src/node/http.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ export const authenticated = (req: express.Request): boolean => {
5252
return true
5353
case AuthType.Password:
5454
// The password is stored in the cookie after being hashed.
55-
return req.args.password && req.cookies.key && safeCompare(req.cookies.key, hash(req.args.password))
55+
return !!(
56+
req.cookies.key &&
57+
(req.args.hashedPassword
58+
? safeCompare(req.cookies.key, req.args.hashedPassword)
59+
: req.args.password && safeCompare(req.cookies.key, hash(req.args.password)))
60+
)
5661
default:
5762
throw new Error(`Unsupported auth type ${req.args.auth}`)
5863
}

src/node/routes/login.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const getRoot = async (req: Request, error?: Error): Promise<string> => {
3030
let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.`
3131
if (req.args.usingEnvPassword) {
3232
passwordMsg = "Password was set from $PASSWORD."
33+
} else if (req.args.usingEnvHashedPassword) {
34+
passwordMsg = "Password was set from $HASHED_PASSWORD."
3335
}
3436
return replaceTemplates(
3537
req,
@@ -65,7 +67,11 @@ router.post("/", async (req, res) => {
6567
throw new Error("Missing password")
6668
}
6769

68-
if (req.args.password && safeCompare(req.body.password, req.args.password)) {
70+
if (
71+
req.args.hashedPassword
72+
? safeCompare(hash(req.body.password), req.args.hashedPassword)
73+
: req.args.password && safeCompare(req.body.password, req.args.password)
74+
) {
6975
// The hash does not add any actual security but we do it for
7076
// obfuscation purposes (and as a side effect it handles escaping).
7177
res.cookie(Cookie.Key, hash(req.body.password), {

test/cli.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe("parser", () => {
2626
port: 8080,
2727
"proxy-domain": [],
2828
usingEnvPassword: false,
29+
usingEnvHashedPassword: false,
2930
"extensions-dir": path.join(paths.data, "extensions"),
3031
"user-data-dir": paths.data,
3132
}
@@ -290,6 +291,21 @@ describe("parser", () => {
290291
})
291292
})
292293

294+
it("should use env var hashed password", async () => {
295+
process.env.HASHED_PASSWORD = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" // test
296+
const args = parse([])
297+
assert.deepEqual(args, {
298+
_: [],
299+
})
300+
301+
assert.deepEqual(await setDefaults(args), {
302+
...defaults,
303+
_: [],
304+
hashedPassword: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
305+
usingEnvHashedPassword: true,
306+
})
307+
})
308+
293309
it("should filter proxy domains", async () => {
294310
const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"])
295311
assert.deepEqual(args, {

0 commit comments

Comments
 (0)