Add hashedPassword config (#2409)

Resolve #2225.
This commit is contained in:
SPGoding 2020-12-08 14:54:17 -06:00 committed by GitHub
parent ff1da17496
commit 1dd7e4b4e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 60 additions and 6 deletions

View File

@ -297,6 +297,9 @@ and then restart `code-server` with:
sudo systemctl restart code-server@$USER sudo systemctl restart code-server@$USER
``` ```
Alternatively, you can specify the SHA-256 of your password at the `hashedPassword` field in the config file.
The `hashedPassword` field takes precedence over `password`.
### How do I securely access development web services? ### How do I securely access development web services?
If you're working on a web service and want to access it locally, `code-server` can proxy it for you. If you're working on a web service and want to access it locally, `code-server` can proxy it for you.

View File

@ -29,6 +29,7 @@ export interface Args extends VsArgs {
config?: string config?: string
auth?: AuthType auth?: AuthType
password?: string password?: string
hashedPassword?: string
cert?: OptionalString cert?: OptionalString
"cert-host"?: string "cert-host"?: string
"cert-key"?: string "cert-key"?: string
@ -104,6 +105,12 @@ const options: Options<Required<Args>> = {
type: "string", type: "string",
description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).", description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
}, },
hashedPassword: {
type: "string",
description:
"The password hashed with SHA-256 for password authentication (can only be passed in via $HASHED_PASSWORD or the config file). \n" +
"Takes precedence over 'password'.",
},
cert: { cert: {
type: OptionalString, type: OptionalString,
path: true, path: true,
@ -279,6 +286,10 @@ export const parse = (
throw new Error("--password can only be set in the config file or passed in via $PASSWORD") throw new Error("--password can only be set in the config file or passed in via $PASSWORD")
} }
if (key === "hashedPassword" && !opts?.configFile) {
throw new Error("--hashedPassword can only be set in the config file or passed in via $HASHED_PASSWORD")
}
const option = options[key] const option = options[key]
if (option.type === "boolean") { if (option.type === "boolean") {
;(args[key] as boolean) = true ;(args[key] as boolean) = true
@ -361,6 +372,7 @@ export interface DefaultedArgs extends ConfigArgs {
"proxy-domain": string[] "proxy-domain": string[]
verbose: boolean verbose: boolean
usingEnvPassword: boolean usingEnvPassword: boolean
usingEnvHashedPassword: boolean
"extensions-dir": string "extensions-dir": string
"user-data-dir": string "user-data-dir": string
} }
@ -448,13 +460,20 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi
args["cert-key"] = certKey args["cert-key"] = certKey
} }
const usingEnvPassword = !!process.env.PASSWORD let usingEnvPassword = !!process.env.PASSWORD
if (process.env.PASSWORD) { if (process.env.PASSWORD) {
args.password = process.env.PASSWORD args.password = process.env.PASSWORD
} }
// Ensure it's not readable by child processes. const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD
if (process.env.HASHED_PASSWORD) {
args.hashedPassword = process.env.HASHED_PASSWORD
usingEnvPassword = false
}
// Ensure they're not readable by child processes.
delete process.env.PASSWORD delete process.env.PASSWORD
delete process.env.HASHED_PASSWORD
// Filter duplicate proxy domains and remove any leading `*.`. // Filter duplicate proxy domains and remove any leading `*.`.
const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, ""))) const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, "")))
@ -463,6 +482,7 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi
return { return {
...args, ...args,
usingEnvPassword, usingEnvPassword,
usingEnvHashedPassword,
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled. } as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
} }

View File

@ -99,8 +99,10 @@ const main = async (args: DefaultedArgs): Promise<void> => {
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`) logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
if (args.auth === AuthType.Password && !args.password) { if (args.auth === AuthType.Password && !args.password && !args.hashedPassword) {
throw new Error("Please pass in a password via the config file or $PASSWORD") throw new Error(
"Please pass in a password via the config file or environment variable ($PASSWORD or $HASHED_PASSWORD)",
)
} }
const [app, wsApp, server] = await createApp(args) const [app, wsApp, server] = await createApp(args)
@ -114,6 +116,8 @@ const main = async (args: DefaultedArgs): Promise<void> => {
logger.info(" - Authentication is enabled") logger.info(" - Authentication is enabled")
if (args.usingEnvPassword) { if (args.usingEnvPassword) {
logger.info(" - Using password from $PASSWORD") logger.info(" - Using password from $PASSWORD")
} else if (args.usingEnvHashedPassword) {
logger.info(" - Using password from $HASHED_PASSWORD")
} else { } else {
logger.info(` - Using password from ${humanPath(args.config)}`) logger.info(` - Using password from ${humanPath(args.config)}`)
} }

View File

@ -52,7 +52,12 @@ export const authenticated = (req: express.Request): boolean => {
return true return true
case AuthType.Password: case AuthType.Password:
// The password is stored in the cookie after being hashed. // The password is stored in the cookie after being hashed.
return req.args.password && req.cookies.key && safeCompare(req.cookies.key, hash(req.args.password)) return !!(
req.cookies.key &&
(req.args.hashedPassword
? safeCompare(req.cookies.key, req.args.hashedPassword)
: req.args.password && safeCompare(req.cookies.key, hash(req.args.password)))
)
default: default:
throw new Error(`Unsupported auth type ${req.args.auth}`) throw new Error(`Unsupported auth type ${req.args.auth}`)
} }

View File

@ -30,6 +30,8 @@ const getRoot = async (req: Request, error?: Error): Promise<string> => {
let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.` let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.`
if (req.args.usingEnvPassword) { if (req.args.usingEnvPassword) {
passwordMsg = "Password was set from $PASSWORD." passwordMsg = "Password was set from $PASSWORD."
} else if (req.args.usingEnvHashedPassword) {
passwordMsg = "Password was set from $HASHED_PASSWORD."
} }
return replaceTemplates( return replaceTemplates(
req, req,
@ -65,7 +67,11 @@ router.post("/", async (req, res) => {
throw new Error("Missing password") throw new Error("Missing password")
} }
if (req.args.password && safeCompare(req.body.password, req.args.password)) { if (
req.args.hashedPassword
? safeCompare(hash(req.body.password), req.args.hashedPassword)
: req.args.password && safeCompare(req.body.password, req.args.password)
) {
// The hash does not add any actual security but we do it for // The hash does not add any actual security but we do it for
// obfuscation purposes (and as a side effect it handles escaping). // obfuscation purposes (and as a side effect it handles escaping).
res.cookie(Cookie.Key, hash(req.body.password), { res.cookie(Cookie.Key, hash(req.body.password), {

View File

@ -26,6 +26,7 @@ describe("parser", () => {
port: 8080, port: 8080,
"proxy-domain": [], "proxy-domain": [],
usingEnvPassword: false, usingEnvPassword: false,
usingEnvHashedPassword: false,
"extensions-dir": path.join(paths.data, "extensions"), "extensions-dir": path.join(paths.data, "extensions"),
"user-data-dir": paths.data, "user-data-dir": paths.data,
} }
@ -290,6 +291,21 @@ describe("parser", () => {
}) })
}) })
it("should use env var hashed password", async () => {
process.env.HASHED_PASSWORD = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" // test
const args = parse([])
assert.deepEqual(args, {
_: [],
})
assert.deepEqual(await setDefaults(args), {
...defaults,
_: [],
hashedPassword: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
usingEnvHashedPassword: true,
})
})
it("should filter proxy domains", async () => { it("should filter proxy domains", async () => {
const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"]) const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"])
assert.deepEqual(args, { assert.deepEqual(args, {