Skip to content

Commit a189b96

Browse files
committed
Add authentication flow
1 parent 57de0f5 commit a189b96

File tree

3 files changed

+120
-5
lines changed

3 files changed

+120
-5
lines changed

src/auth.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// import * as assert from "assert"
2+
import * as vscode from "vscode"
3+
// import * as auth from "./download"
4+
5+
suite("Authenticate", () => {
6+
vscode.window.showInformationMessage("Start authenticate tests.")
7+
8+
// TODO: Implement.
9+
test("authenticate")
10+
})

src/auth.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { promises as fs } from "fs"
2+
import * as os from "os"
3+
import * as path from "path"
4+
import * as vscode from "vscode"
5+
import { debug } from "./utils"
6+
7+
const getConfigDir = (): string => {
8+
// The CLI uses localConfig from https://github.com/kirsle/configdir.
9+
switch (process.platform) {
10+
case "win32":
11+
return process.env.APPDATA || path.join(os.homedir(), "AppData/Roaming")
12+
case "darwin":
13+
return path.join(os.homedir(), "Library/Application Support")
14+
case "linux":
15+
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
16+
}
17+
throw new Error(`Unsupported platform ${process.platform}`)
18+
}
19+
20+
/**
21+
* Authenticate the Coder CLI.
22+
*/
23+
const doAuthenticate = async (accessUrl?: string, token?: string): Promise<void> => {
24+
if (!accessUrl) {
25+
debug(` - No access URL, querying user`)
26+
accessUrl = await vscode.window.showInputBox({
27+
prompt: "Coder URL",
28+
placeHolder: "https://my.coder.domain",
29+
})
30+
if (!accessUrl) {
31+
throw new Error("Unable to authenticate; no access URL was provided")
32+
}
33+
}
34+
35+
// TODO: This step can be automated if we make the internal-auth endpoint
36+
// automatically open another VS Code URI.
37+
if (!token) {
38+
debug(` - No token, querying user`)
39+
const url = vscode.Uri.parse(`${accessUrl}/internal-auth?show_token=true`)
40+
const opened = await vscode.env.openExternal(url)
41+
debug(` - Opened ${url}: ${opened}`)
42+
token = await vscode.window.showInputBox({
43+
ignoreFocusOut: true,
44+
placeHolder: "Paste your token here",
45+
prompt: `Token from ${url.toString(true)}`,
46+
})
47+
if (!token) {
48+
throw new Error("Unable to authenticate; no token was provided")
49+
}
50+
}
51+
52+
// TODO: Using the login command would be ideal but it unconditionally opens a
53+
// browser. To work around this write to the config files directly. We
54+
// cannot use the env-paths module because the library the CLI is using
55+
// implements both Windows and macOS paths differently.
56+
const dir = path.join(getConfigDir(), "coder")
57+
await fs.mkdir(dir, { recursive: true })
58+
await Promise.all([fs.writeFile(path.join(dir, "session"), token), fs.writeFile(path.join(dir, "url"), accessUrl)])
59+
}
60+
61+
/** Only allow one at a time. */
62+
let promise: Promise<void> | undefined
63+
64+
export const authenticate = async (accessUrl?: string, token?: string): Promise<void> => {
65+
if (!promise) {
66+
promise = (async (): Promise<void> => {
67+
try {
68+
return await doAuthenticate(accessUrl, token)
69+
} finally {
70+
promise = undefined
71+
}
72+
})()
73+
}
74+
75+
return promise
76+
}

src/exec.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from "path"
33
import * as stream from "stream"
44
import { promisify } from "util"
55
import * as nodeWhich from "which"
6+
import { authenticate } from "./auth"
67
import { download } from "./download"
78
import { context, debug } from "./utils"
89

@@ -36,12 +37,40 @@ export const execCoder = async (command: string, opts?: CoderOptions): Promise<s
3637
debug(`Run command: ${command}`)
3738

3839
const invocation = coderInvocation()
39-
const cmd = (await binaryExists(invocation.cmd))
40-
? [invocation.cmd, ...invocation.args].join(" ")
41-
: await download(opts?.version || "latest", path.join(await context().globalStoragePath, invocation.cmd))
40+
let coderBinary = [invocation.cmd, ...invocation.args].join(" ")
4241

43-
const output = await promisify(cp.exec)(cmd + " " + command)
44-
return output.stdout
42+
try {
43+
if (!(await binaryExists(invocation.cmd))) {
44+
coderBinary = await download(
45+
opts?.version || "latest",
46+
path.join(await context().globalStoragePath, invocation.cmd),
47+
)
48+
}
49+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50+
} catch (error: any) {
51+
// Re-throw with some guidance on how to manually install.
52+
throw new Error(`${error.message.trim()}. Please [install manually](https://coder.com/docs/cli/installation).`)
53+
}
54+
55+
try {
56+
const output = await promisify(cp.exec)(coderBinary + " " + command)
57+
return output.stdout
58+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59+
} catch (error: any) {
60+
// See if the error appears to be related to the token or login. If it does
61+
// we will try authenticating then run the command again.
62+
// TODO: Since this relies on stderr output being a certain way it might be
63+
// better if the CLI had a command for checking the login status.
64+
if (/Session-Token|credentials|API key|401/.test(error.stderr)) {
65+
await authenticate(opts?.accessUri, opts?.token)
66+
const output = await promisify(cp.exec)(coderBinary + " " + command)
67+
return output.stdout
68+
} else {
69+
// Otherwise it is some other kind of error, like the command does not
70+
// exist or the binary is gone, etc.
71+
throw error
72+
}
73+
}
4574
}
4675

4776
/**

0 commit comments

Comments
 (0)