From d911eac5f26130158ef1e3e1acf2003c23e8e13f Mon Sep 17 00:00:00 2001
From: Gabe Rudy <rudy@goldenhelix.com>
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<T> = {
 
 export const options: Options<Required<UserProvidedArgs>> = {
   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<boolean> => {
 
       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 <rudy@goldenhelix.com>
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<T> = {
 export const options: Options<Required<UserProvidedArgs>> = {
   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<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;
+    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<boolean> => {
       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,