Skip to content

Commit 6091d84

Browse files
committed
fix login and out of same api key bug
1 parent 609c78a commit 6091d84

8 files changed

Lines changed: 199 additions & 516 deletions

File tree

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,20 @@
6767
]
6868
}
6969
}
70-
}
70+
},
71+
"commands": [{
72+
"title": "Login",
73+
"category": "Socket Security",
74+
"command": "socket-security.login"
75+
}]
7176
},
7277
"bugs": {
7378
"email": "support@socket.dev"
7479
},
7580
"publisher": "SocketSecurity",
7681
"scripts": {
7782
"vscode:prepublish": "npm run esbuild -- --minify",
83+
"vscode:uninstall": "node ./src/lifecycle/uninstall.mjs",
7884
"esbuild-base": "esbuild --bundle --external:tree-sitter-java --external:vscode --loader:.wasm=binary --loader:.go=file --loader:.py=text --outdir=out/ --platform=node --sourcemap",
7985
"esbuild": "npm run esbuild-base -- --format=cjs main=src/extension.ts",
8086
"test-compile": "tsc -p ./",

src/auth.ts

Lines changed: 171 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2,69 +2,123 @@ import vscode from 'vscode'
22
import os from 'os'
33
import path from 'path'
44
import { DIAGNOSTIC_SOURCE_STR, EXTENSION_PREFIX } from './util'
5-
import { getQuota } from './api'
5+
import constants from '@socketsecurity/registry/lib/constants'
6+
import https from 'node:https'
7+
import { once } from 'node:events'
8+
import { IncomingMessage } from 'node:http'
9+
import { text } from 'node:stream/consumers'
10+
import fs from 'fs'
11+
import { randomUUID } from 'node:crypto'
12+
const { SOCKET_PUBLIC_API_TOKEN } = constants
13+
export type APIConfig = {
14+
apiKey: string
15+
}
16+
17+
type OrgInfo = {
18+
id: string
19+
name: string
20+
image: string | null
21+
plan: 'opensource' | 'team' | 'enterprise'
22+
}
23+
24+
type OrganizationsRecord = {
25+
organizations: Record<string, OrgInfo>
26+
}
27+
28+
async function getOrganizations(apiKey: string): Promise<OrganizationsRecord | null> {
29+
const authHeader = getAuthHeader(apiKey)
30+
const orgReq = https.get('https://api.socket.dev/v0/organizations', {
31+
method: 'GET',
32+
headers: {
33+
Authorization: authHeader,
34+
'Content-Type': 'application/json'
35+
}
36+
})
37+
const [orgRes] = await once(orgReq, 'response') as [IncomingMessage]
38+
if (orgRes.statusCode !== 200) {
39+
return null
40+
}
41+
const orgs: OrganizationsRecord = JSON.parse(await text(orgRes))
42+
return orgs
43+
}
644

7-
export async function activate(context: vscode.ExtensionContext, disposables?: Array<vscode.Disposable>) {
45+
export async function activate(context: vscode.ExtensionContext, disposables: Array<vscode.Disposable>) {
846
//#region file path/watching
947
// responsible for watching files to know when to sync from disk
1048
let dataHome = process.platform === 'win32'
11-
? process.env['LOCALAPPDATA']
12-
: process.env['XDG_DATA_HOME']
13-
49+
? process.env['LOCALAPPDATA']
50+
: process.env['XDG_DATA_HOME']
51+
1452
if (!dataHome) {
15-
if (process.platform === 'win32') throw new Error('missing %LOCALAPPDATA%')
16-
const home = os.homedir()
17-
dataHome = path.join(home, ...(process.platform === 'darwin'
18-
? ['Library', 'Application Support']
19-
: ['.local', 'share']
20-
))
53+
if (process.platform === 'win32') throw new Error('missing %LOCALAPPDATA%')
54+
const home = os.homedir()
55+
dataHome = path.join(home, ...(process.platform === 'darwin'
56+
? ['Library', 'Application Support']
57+
: ['.local', 'share']
58+
))
2159
}
22-
60+
let pleaseLoginStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100)
61+
pleaseLoginStatusBar.hide()
62+
pleaseLoginStatusBar.text = `$(warning) Socket Security: Login`
63+
pleaseLoginStatusBar.tooltip = 'Socket Security needs to login for full functionality'
64+
pleaseLoginStatusBar.command = `${EXTENSION_PREFIX}.login`
65+
2366
let defaultSettingsPath = path.join(dataHome, 'socket', 'settings')
2467
let settingsPath = vscode.workspace.getConfiguration(EXTENSION_PREFIX)
2568
.get('settingsFile', defaultSettingsPath)
2669
//#endregion
2770
//#region session sync
2871
// responsible for keeping disk an mem in sync
29-
const PUBLIC_TOKEN = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api'
3072
let liveSessions: Map<vscode.AuthenticationSession['accessToken'], vscode.AuthenticationSession> = new Map()
31-
const emitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>()
32-
async function syncLiveSessionsFromDisk() {
33-
const settings_on_disk = JSON.parse(Buffer.from(
34-
new TextDecoder().decode(await vscode.workspace.fs.readFile(vscode.Uri.file(settingsPath))),
35-
'base64'
36-
).toString('utf8'))
73+
const diskSessionsChanges = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>()
74+
75+
const watcher = vscode.workspace.createFileSystemWatcher(
76+
new vscode.RelativePattern(
77+
path.dirname(settingsPath),
78+
path.basename(settingsPath)
79+
)
80+
)
81+
disposables?.push(
82+
watcher,
83+
watcher.onDidChange(() => syncLiveSessionFromDisk()),
84+
watcher.onDidCreate(() => syncLiveSessionFromDisk()),
85+
watcher.onDidDelete(() => { syncLiveSessionFromDisk() })
86+
)
87+
async function syncLiveSessionFromDisk() {
88+
let settings_on_disk: {apiKey?: string | null} = {
89+
apiKey: null
90+
}
91+
try {
92+
let fromDisk = JSON.parse(Buffer.from(
93+
new TextDecoder().decode(await vscode.workspace.fs.readFile(vscode.Uri.file(settingsPath))),
94+
'base64'
95+
).toString('utf8'))
96+
if (fromDisk && typeof fromDisk === 'object' && fromDisk !== null) {
97+
settings_on_disk = fromDisk
98+
}
99+
} catch {}
37100
const {
38101
apiKey
39102
} = settings_on_disk
40103
const sessionOnDisk: typeof liveSessions = new Map<vscode.AuthenticationSession['accessToken'], vscode.AuthenticationSession>()
41-
if (apiKey) {
42-
sessionOnDisk.set(
43-
apiKey,
44-
{
45-
accessToken: apiKey,
46-
id: apiKey,
47-
account: {
48-
id: apiKey,
49-
label: `API Key for ${DIAGNOSTIC_SOURCE_STR}`
50-
},
51-
scopes: [],
52-
}
53-
)
104+
if (typeof apiKey === 'string' && apiKey.length > 0 && apiKey !== SOCKET_PUBLIC_API_TOKEN) {
105+
const organizations = await getOrganizations(apiKey)
106+
const org = Object.values(organizations!.organizations)[0]
107+
if (org) {
108+
sessionOnDisk.set(
109+
apiKey,
110+
sessionFromAPIKey(apiKey, org)
111+
)
112+
}
54113
}
55114
let added: Array<vscode.AuthenticationSession> = []
56115
let changed: Array<vscode.AuthenticationSession> = []
57116
let removed: Array<vscode.AuthenticationSession> = []
58117
for (const diskSession of sessionOnDisk.values()) {
59118
// already have this access token in mem session
119+
// remove from live sessions that haven't been sorted
60120
if (liveSessions.has(diskSession.accessToken)) {
61-
const liveSession = liveSessions.get(diskSession.accessToken)
62121
liveSessions.delete(diskSession.accessToken)
63-
// mem has same as what is on disk
64-
if (JSON.stringify(liveSession) !== JSON.stringify(diskSession)) {
65-
continue
66-
}
67-
changed.push(diskSession)
68122
} else {
69123
added.push(diskSession)
70124
}
@@ -74,110 +128,132 @@ export async function activate(context: vscode.ExtensionContext, disposables?: A
74128
}
75129
liveSessions = sessionOnDisk
76130
if (added.length + changed.length + removed.length > 0) {
77-
emitter.fire({
131+
diskSessionsChanges.fire({
78132
added,
79133
changed,
80134
removed
81135
})
82136
}
83137
}
84-
async function syncLiveSessionsToDisk() {
138+
async function syncLiveSessionToDisk(session: vscode.AuthenticationSession) {
139+
if (!session || !session.accessToken || session.accessToken === SOCKET_PUBLIC_API_TOKEN) {
140+
return
141+
}
85142
const contents = Buffer.from(
86-
JSON.stringify(
87-
Array.from(liveSessions.values(), s => ({
88-
apiKey: s.accessToken
89-
})),
90-
null,
91-
2
92-
)
143+
JSON.stringify({
144+
apiKey: session.accessToken
145+
})
93146
).toString('base64')
94147
return vscode.workspace.fs.writeFile(vscode.Uri.file(settingsPath), new TextEncoder().encode(contents))
95148
}
96-
await syncLiveSessionsFromDisk()
97149
//#endregion
98150
//#region service glue
99151
const service = vscode.authentication.registerAuthenticationProvider(`${EXTENSION_PREFIX}`, `${DIAGNOSTIC_SOURCE_STR}`, {
100152
onDidChangeSessions(fn) {
101-
return emitter.event(fn)
153+
return diskSessionsChanges.event(fn);
102154
},
103155
async getSessions(scopes: readonly string[] | undefined, options: vscode.AuthenticationProviderSessionOptions): Promise<vscode.AuthenticationSession[]> {
104156
return Array.from(liveSessions.values())
105157
},
106158
async createSession(scopes: readonly string[], options: vscode.AuthenticationProviderSessionOptions): Promise<vscode.AuthenticationSession> {
107-
const realLogin = `Log in to ${DIAGNOSTIC_SOURCE_STR}`
108-
const publicLogin = `Use public token for ${DIAGNOSTIC_SOURCE_STR}`
109-
const res = await vscode.window.showQuickPick([
110-
realLogin,
111-
publicLogin,
112-
])
113-
if (!res) {
114-
throw new Error(`Cancelled creation of session for ${DIAGNOSTIC_SOURCE_STR}`)
115-
}
116-
let apiKey: string
117-
if (res === publicLogin) {
118-
apiKey = ''
119-
} else {
120-
let keyInfo: string
121-
let maybeApiKey = await vscode.window.showInputBox({
122-
title: 'Socket Security API Token',
123-
placeHolder: 'Leave this blank to use public demo token',
124-
prompt: 'Enter your API token from https://socket.dev/',
125-
async validateInput (value) {
126-
if (!value) return
127-
keyInfo = (await getQuota(value))!
128-
if (!keyInfo) return 'Unable to validate API key'
129-
}
130-
})
131-
// cancelled
132-
if (maybeApiKey === undefined) {
133-
throw new Error(`Cancelled creation of session for ${DIAGNOSTIC_SOURCE_STR}`)
159+
let organizations: OrganizationsRecord
160+
let apiKey: string = await vscode.window.showInputBox({
161+
title: 'Socket Security API Token',
162+
placeHolder: 'Leave this blank to stay logged out',
163+
ignoreFocusOut: true,
164+
prompt: 'Enter your API token from https://socket.dev/',
165+
async validateInput(value) {
166+
if (!value) return
167+
organizations = (await getOrganizations(value))!
168+
if (!organizations) return 'Invalid API key'
134169
}
135-
apiKey = maybeApiKey
136-
}
137-
if (apiKey === '') {
138-
apiKey = PUBLIC_TOKEN
139-
}
140-
const session = {
141-
accessToken: apiKey,
142-
id: apiKey,
143-
account: {
144-
id: apiKey,
145-
label: `API Key for ${DIAGNOSTIC_SOURCE_STR}`
146-
},
147-
scopes: [],
170+
}) ?? ''
171+
if (!apiKey) {
172+
throw new Error('User did not want to provide an API key')
148173
}
174+
const org = Object.values(organizations!.organizations)[0]
175+
const session = sessionFromAPIKey(apiKey, org)
149176
let oldSessions = Array.from(liveSessions.values())
177+
await syncLiveSessionToDisk(session)
150178
liveSessions = new Map([
151179
[apiKey, session]
152180
])
153-
emitter.fire({
181+
pleaseLoginStatusBar.hide()
182+
diskSessionsChanges.fire({
154183
added: [session],
155184
changed: [],
156185
removed: oldSessions
157186
})
158-
await syncLiveSessionsToDisk()
159187
return session
160188
},
161189
async removeSession(sessionId: string): Promise<void> {
162190
const session = liveSessions.get(sessionId)
191+
try {
192+
pleaseLoginStatusBar.show()
193+
} catch {}
194+
try {
195+
fs.unlinkSync(settingsPath)
196+
} catch {}
163197
if (session) {
164-
emitter.fire({
198+
diskSessionsChanges.fire({
165199
added: [],
166200
changed: [],
167201
removed: [session]
168202
})
169-
await syncLiveSessionsToDisk()
170203
}
171204
}
172205
})
173206
context.subscriptions.push(service)
174-
vscode.commands.registerCommand(`${EXTENSION_PREFIX}.login`, () => {
175-
vscode.authentication.getSession(`${EXTENSION_PREFIX}`, [], {
207+
vscode.commands.registerCommand(`${EXTENSION_PREFIX}.login`, async () => {
208+
let session = await vscode.authentication.getSession(`${EXTENSION_PREFIX}`, [], {
176209
createIfNone: true,
177210
})
178211
})
212+
try {
213+
await syncLiveSessionFromDisk()
214+
} catch {}
215+
let session
216+
try {
217+
session = await vscode.authentication.getSession(`${EXTENSION_PREFIX}`, [], {
218+
createIfNone: false
219+
})
220+
} catch {}
221+
if (!session) {
222+
pleaseLoginStatusBar.show()
223+
}
179224
//#endregion
180225
return {
181226

182227
}
183228
}
229+
230+
export async function getAPIKey() {
231+
const session = await vscode.authentication.getSession(`${EXTENSION_PREFIX}`, [], {
232+
createIfNone: false,
233+
})
234+
if (session) {
235+
return session?.accessToken
236+
} else {
237+
return SOCKET_PUBLIC_API_TOKEN
238+
}
239+
}
240+
241+
export function getAuthHeader(apiKey: string) {
242+
return `Bearer ${apiKey}`
243+
}
244+
function sessionFromAPIKey(apiKey: string, org: OrgInfo) {
245+
// vscode auth does weird caching based upon ids
246+
// if we don't change the id various things stop working
247+
// like logging in and out with same account/api token
248+
const uniqueId = `${apiKey}-${randomUUID()}`
249+
return{
250+
accessToken: apiKey,
251+
id: `${uniqueId}.session`,
252+
account: {
253+
id: `${apiKey}.account`,
254+
label: `${org.name} (${org.plan})`
255+
},
256+
scopes: [],
257+
}
258+
}
259+

0 commit comments

Comments
 (0)