From d911eac5f26130158ef1e3e1acf2003c23e8e13f Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Sun, 22 Dec 2024 13:04:07 -0700 Subject: [PATCH 1/2] Support HTTP BasicAuth for authentication if $AUTH_USER is set --- src/node/cli.ts | 10 ++++++++++ src/node/http.ts | 22 ++++++++++++++++++++++ src/node/main.ts | 4 ++++ src/node/routes/domainProxy.ts | 5 +++++ src/node/routes/pathProxy.ts | 3 ++- src/node/routes/vscode.ts | 8 +++++++- 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 9eb6e5163e8a..aace0b59a0eb 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -12,6 +12,7 @@ export enum Feature { export enum AuthType { Password = "password", + HttpBasic = "http-basic", None = "none", } @@ -65,6 +66,7 @@ export interface UserProvidedCodeArgs { export interface UserProvidedArgs extends UserProvidedCodeArgs { config?: string auth?: AuthType + "auth-user"?: string password?: string "hashed-password"?: string cert?: OptionalString @@ -137,6 +139,10 @@ export type Options = { export const options: Options> = { auth: { type: AuthType, description: "The type of authentication to use." }, + "auth-user": { + type: "string", + description: "The username for http-basic authentication." + }, password: { type: "string", description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).", @@ -569,6 +575,10 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config if (process.env.PASSWORD) { args.password = process.env.PASSWORD } + if (process.env.AUTH_USER) { + args["auth"] = AuthType.HttpBasic + args["auth-user"] = process.env.AUTH_USER + } if (process.env.CS_DISABLE_FILE_DOWNLOADS?.match(/^(1|true)$/)) { args["disable-file-downloads"] = true diff --git a/src/node/http.ts b/src/node/http.ts index e0fb3a4caf6b..88dad9c255fd 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -111,6 +111,25 @@ export const ensureAuthenticated = async ( } } +/** + * Validate basic auth credentials. + */ +const validateBasicAuth = (authHeader: string | undefined, authUser: string | undefined, authPassword: string | undefined): boolean => { + if (!authHeader?.startsWith('Basic ')) { + return false; + } + + try { + const base64Credentials = authHeader.split(' ')[1]; + const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); + const [username, password] = credentials.split(':'); + return username === authUser && password === authPassword; + } catch (error) { + logger.error('Error validating basic auth:' + error); + return false; + } +}; + /** * Return true if authenticated via cookies. */ @@ -132,6 +151,9 @@ export const authenticated = async (req: express.Request): Promise => { return await isCookieValid(isCookieValidArgs) } + case AuthType.HttpBasic: { + return validateBasicAuth(req.headers.authorization, req.args["auth-user"], req.args.password); + } default: { throw new Error(`Unsupported auth type ${req.args.auth}`) } diff --git a/src/node/main.ts b/src/node/main.ts index b3c4e4c14500..5c02bf0eb653 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -142,6 +142,10 @@ export const runCodeServer = async ( } else { logger.info(` - Using password from ${args.config}`) } + } else if (args.auth === AuthType.HttpBasic) { + logger.info(" - HTTP basic authentication is enabled") + logger.info(" - Using user from $AUTH_USER") + logger.info(" - Using password from $PASSWORD") } else { logger.info(" - Authentication is disabled") } diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 0a9bb4a324f7..05624a9f7972 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -3,6 +3,7 @@ import { HttpCode, HttpError } from "../../common/http" import { getHost, ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" +import { AuthType } from "../cli" export const router = Router() @@ -78,6 +79,10 @@ router.all(/.*/, async (req, res, next) => { if (/\/login\/?/.test(req.path)) { return next() } + // If auth is HttpBasic, return a 401. + if (req.args.auth === AuthType.HttpBasic) { + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } // Redirect all other pages to the login. const to = self(req) return redirect(req, res, "login", { diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts index ccfb0cc824a0..848a514f6243 100644 --- a/src/node/routes/pathProxy.ts +++ b/src/node/routes/pathProxy.ts @@ -4,6 +4,7 @@ import * as pluginapi from "../../../typings/pluginapi" import { HttpCode, HttpError } from "../../common/http" import { ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy as _proxy } from "../proxy" +import { AuthType } from "../cli" const getProxyTarget = ( req: Request, @@ -28,7 +29,7 @@ export async function proxy( if (!(await authenticated(req))) { // If visiting the root (/:port only) redirect to the login page. - if (!req.params.path || req.params.path === "/") { + if ((!req.params.path || req.params.path === "/") && req.args.auth !== AuthType.HttpBasic) { const to = self(req) return redirect(req, res, "login", { to: to !== "/" ? to : undefined, diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index 7e8f0f3ff4e5..d2bd8e120aad 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -7,12 +7,13 @@ import * as net from "net" import * as path from "path" import { WebsocketRequest } from "../../../typings/pluginapi" import { logError } from "../../common/util" -import { CodeArgs, toCodeArgs } from "../cli" +import { AuthType, CodeArgs, toCodeArgs } from "../cli" import { isDevMode, vsRootPath } from "../constants" import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemplates, self } from "../http" import { SocketProxyProvider } from "../socket" import { isFile } from "../util" import { Router as WsRouter } from "../wsRouter" +import { HttpCode, HttpError } from "../../common/http" export const router = express.Router() @@ -118,6 +119,11 @@ router.get("/", ensureVSCodeLoaded, async (req, res, next) => { const FOLDER_OR_WORKSPACE_WAS_CLOSED = req.query.ew if (!isAuthenticated) { + // If auth is HttpBasic, return a 401. + if (req.args.auth === AuthType.HttpBasic) { + res.setHeader('WWW-Authenticate', 'Basic realm="Access to the site"') + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + }; const to = self(req) return redirect(req, res, "login", { to: to !== "/" ? to : undefined, From 6448408fc47bd4e38c9c1239774d5afb1773e396 Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Sun, 19 Jan 2025 16:51:16 -0700 Subject: [PATCH 2/2] Support hashed password for basic auth and match style --- src/node/cli.ts | 10 +++++++-- src/node/http.ts | 39 ++++++++++++++++++++++++---------- src/node/main.ts | 13 +++++++----- src/node/routes/domainProxy.ts | 2 +- src/node/routes/vscode.ts | 6 +++--- 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index aace0b59a0eb..60136913258c 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -140,8 +140,8 @@ export type Options = { export const options: Options> = { auth: { type: AuthType, description: "The type of authentication to use." }, "auth-user": { - type: "string", - description: "The username for http-basic authentication." + type: "string", + description: "The username for http-basic authentication.", }, password: { type: "string", @@ -486,6 +486,7 @@ export interface DefaultedArgs extends ConfigArgs { "proxy-domain": string[] verbose: boolean usingEnvPassword: boolean + usingEnvAuthUser: boolean usingEnvHashedPassword: boolean "extensions-dir": string "user-data-dir": string @@ -575,9 +576,13 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config if (process.env.PASSWORD) { args.password = process.env.PASSWORD } + + const usingEnvAuthUser = !!process.env.AUTH_USER if (process.env.AUTH_USER) { args["auth"] = AuthType.HttpBasic args["auth-user"] = process.env.AUTH_USER + } else if (args["auth-user"]) { + args["auth"] = AuthType.HttpBasic } if (process.env.CS_DISABLE_FILE_DOWNLOADS?.match(/^(1|true)$/)) { @@ -631,6 +636,7 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config return { ...args, usingEnvPassword, + usingEnvAuthUser, usingEnvHashedPassword, } as DefaultedArgs // TODO: Technically no guarantee this is fulfilled. } diff --git a/src/node/http.ts b/src/node/http.ts index 88dad9c255fd..28419c6d6886 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -4,6 +4,7 @@ import * as expressCore from "express-serve-static-core" import * as http from "http" import * as net from "net" import * as qs from "qs" +import safeCompare from "safe-compare" import { Disposable } from "../common/emitter" import { CookieKeys, HttpCode, HttpError } from "../common/http" import { normalize } from "../common/util" @@ -20,6 +21,7 @@ import { escapeHtml, escapeJSON, splitOnFirstEquals, + isHashMatch, } from "./util" /** @@ -114,21 +116,31 @@ export const ensureAuthenticated = async ( /** * Validate basic auth credentials. */ -const validateBasicAuth = (authHeader: string | undefined, authUser: string | undefined, authPassword: string | undefined): boolean => { - if (!authHeader?.startsWith('Basic ')) { - return false; +const validateBasicAuth = async ( + authHeader: string | undefined, + authUser: string | undefined, + authPassword: string | undefined, + hashedPassword: string | undefined, +): Promise => { + if (!authHeader?.startsWith("Basic ")) { + return false } try { - const base64Credentials = authHeader.split(' ')[1]; - const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); - const [username, password] = credentials.split(':'); - return username === authUser && password === authPassword; + const base64Credentials = authHeader.split(" ")[1] + const credentials = Buffer.from(base64Credentials, "base64").toString("utf-8") + const [username, password] = credentials.split(":") + if (username !== authUser) return false + if (hashedPassword) { + return await isHashMatch(password, hashedPassword) + } else { + return safeCompare(password, authPassword || "") + } } catch (error) { - logger.error('Error validating basic auth:' + error); - return false; + logger.error("Error validating basic auth:" + error) + return false } -}; +} /** * Return true if authenticated via cookies. @@ -152,7 +164,12 @@ export const authenticated = async (req: express.Request): Promise => { return await isCookieValid(isCookieValidArgs) } case AuthType.HttpBasic: { - return validateBasicAuth(req.headers.authorization, req.args["auth-user"], req.args.password); + return await validateBasicAuth( + req.headers.authorization, + req.args["auth-user"], + req.args.password, + req.args["hashed-password"], + ) } default: { throw new Error(`Unsupported auth type ${req.args.auth}`) diff --git a/src/node/main.ts b/src/node/main.ts index 5c02bf0eb653..a8c8560e18cc 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -133,7 +133,7 @@ export const runCodeServer = async ( logger.info(`Using config file ${args.config}`) logger.info(`${protocol.toUpperCase()} server listening on ${serverAddress.toString()}`) - if (args.auth === AuthType.Password) { + if (args.auth === AuthType.Password || args.auth === AuthType.HttpBasic) { logger.info(" - Authentication is enabled") if (args.usingEnvPassword) { logger.info(" - Using password from $PASSWORD") @@ -142,10 +142,13 @@ export const runCodeServer = async ( } else { logger.info(` - Using password from ${args.config}`) } - } else if (args.auth === AuthType.HttpBasic) { - logger.info(" - HTTP basic authentication is enabled") - logger.info(" - Using user from $AUTH_USER") - logger.info(" - Using password from $PASSWORD") + if (args.auth === AuthType.HttpBasic) { + if (args.usingEnvAuthUser) { + logger.info(" - Using user from $AUTH_USER") + } else { + logger.info(` - With user ${args["auth-user"]}`) + } + } } else { logger.info(" - Authentication is disabled") } diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 05624a9f7972..e2af5cc4dfac 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -1,9 +1,9 @@ import { Request, Router } from "express" import { HttpCode, HttpError } from "../../common/http" +import { AuthType } from "../cli" import { getHost, ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" -import { AuthType } from "../cli" export const router = Router() diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index d2bd8e120aad..7e04d5dad49d 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -6,6 +6,7 @@ import * as http from "http" import * as net from "net" import * as path from "path" import { WebsocketRequest } from "../../../typings/pluginapi" +import { HttpCode, HttpError } from "../../common/http" import { logError } from "../../common/util" import { AuthType, CodeArgs, toCodeArgs } from "../cli" import { isDevMode, vsRootPath } from "../constants" @@ -13,7 +14,6 @@ import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemp import { SocketProxyProvider } from "../socket" import { isFile } from "../util" import { Router as WsRouter } from "../wsRouter" -import { HttpCode, HttpError } from "../../common/http" export const router = express.Router() @@ -121,9 +121,9 @@ router.get("/", ensureVSCodeLoaded, async (req, res, next) => { if (!isAuthenticated) { // If auth is HttpBasic, return a 401. if (req.args.auth === AuthType.HttpBasic) { - res.setHeader('WWW-Authenticate', 'Basic realm="Access to the site"') + res.setHeader("WWW-Authenticate", `Basic realm="${req.args["app-name"] || "code-server"}"`) throw new HttpError("Unauthorized", HttpCode.Unauthorized) - }; + } const to = self(req) return redirect(req, res, "login", { to: to !== "/" ? to : undefined,