Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ socket patch setup --yes

Free patches work without authentication. For paid patches, set `SOCKET_CLI_API_TOKEN` and `SOCKET_CLI_ORG_SLUG`.

`socket login` uses browser OAuth for read-only package, alert, and threat-feed
access. Open the printed authorization URL manually if a browser does not open.
Use `socket login --api-token` or `SOCKET_CLI_API_TOKEN` for scans and write
commands.

### Aliases

All aliases support the flags and arguments of the commands they alias.
Expand Down
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ module.exports = [
// Allow paths like src/commands/manifest/bazel/*.test.mts.
'src/*/*/*/*.test.mts',
'test/*.mts',
'test/src/utils/*.test.mts',
'test/src/commands/logout/*.test.mts',
// Allow loose one-off scripts.
'scripts/*.mts',
],
Expand Down
1 change: 0 additions & 1 deletion scripts/ci/setup-node.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { execFile as execFileCallback } from 'node:child_process'
import { createHash } from 'node:crypto'
import { constants } from 'node:fs'
Expand Down
2 changes: 1 addition & 1 deletion src/commands/cli.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('socket root command', async () => {
Note: All commands have their own --help

Main commands
socket login Setup Socket CLI with an API token and defaults
socket login Sign in to Socket and configure the CLI
socket scan create Create a new Socket scan and report
socket npm/lodash@4.17.21 Request the Socket score of a package
socket fix Fix CVEs in dependencies
Expand Down
42 changes: 41 additions & 1 deletion src/commands/config/cmd-config-set.test.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs'
import os from 'node:os'
import path from 'node:path'

Expand Down Expand Up @@ -164,4 +170,38 @@ describe('socket config get', async () => {
}
},
)

cmdit(
['config', 'set', 'apiToken', 'skt_faketoken'],
'should activate API-token auth when replacing a saved OAuth login',
async cmd => {
const dataHome = mkdtempSync(path.join(os.tmpdir(), 'socket-cfg-'))
const configPath = path.join(
dataHome,
'socket',
'settings',
'config.json',
)
mkdirSync(path.dirname(configPath), { recursive: true })
writeFileSync(
configPath,
Buffer.from(JSON.stringify({ authType: 'oauth' })).toString('base64'),
)

try {
const { code, stdout } = await spawnSocketCli(binCliPath, cmd, {
env: { XDG_DATA_HOME: dataHome },
})
expect(code, 'a persistable set should exit 0').toBe(0)
expect(stdout).toContain('OK')

const raw = readFileSync(configPath, 'utf8')
const saved = JSON.parse(Buffer.from(raw, 'base64').toString('utf8'))
expect(saved.apiToken).toBe('skt_faketoken')
expect(saved.authType).toBe('api-token')
} finally {
rmSync(dataHome, { recursive: true, force: true })
}
},
)
})
6 changes: 6 additions & 0 deletions src/commands/config/handle-config-set.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug'

import { outputConfigSet } from './output-config-set.mts'
import { CONFIG_KEY_API_TOKEN, CONFIG_KEY_AUTH_TYPE } from '../../constants.mts'
import { updateConfigValue } from '../../utils/config.mts'

import type { CResult, OutputKind } from '../../types.mts'
Expand All @@ -19,6 +20,11 @@ export async function handleConfigSet({
debugDir('inspect', { key, value, outputKind })

const result = updateConfigValue(key, value)
if (result.ok && !result.data && key === CONFIG_KEY_API_TOKEN) {
// An explicit API-token update must take precedence over a saved OAuth
// session, which otherwise remains the active credential source.
updateConfigValue(CONFIG_KEY_AUTH_TYPE, 'api-token')
}

// `config set` is a one-shot command: an in-memory-only change is a no-op
// because the process exits before anything reads it. updateConfigValue only
Expand Down
2 changes: 2 additions & 0 deletions src/commands/login/apply-login.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CONFIG_KEY_API_BASE_URL,
CONFIG_KEY_API_PROXY,
CONFIG_KEY_API_TOKEN,
CONFIG_KEY_AUTH_TYPE,
CONFIG_KEY_ENFORCED_ORGS,
} from '../../constants.mts'
import { updateConfigValue } from '../../utils/config.mts'
Expand All @@ -14,6 +15,7 @@ export function applyLogin(
) {
updateConfigValue(CONFIG_KEY_ENFORCED_ORGS, enforcedOrgs)
updateConfigValue(CONFIG_KEY_API_TOKEN, apiToken)
updateConfigValue(CONFIG_KEY_AUTH_TYPE, 'api-token')
updateConfigValue(CONFIG_KEY_API_BASE_URL, apiBaseUrl)
updateConfigValue(CONFIG_KEY_API_PROXY, apiProxy)
}
144 changes: 141 additions & 3 deletions src/commands/login/attempt-login.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import constants, {
CONFIG_KEY_API_BASE_URL,
CONFIG_KEY_API_PROXY,
CONFIG_KEY_API_TOKEN,
CONFIG_KEY_AUTH_TYPE,
CONFIG_KEY_DEFAULT_ORG,
} from '../../constants.mts'
import {
Expand All @@ -15,8 +16,18 @@ import {
updateConfigValue,
} from '../../utils/config.mts'
import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts'
import {
OAUTH_ISSUER,
beginOAuthLogin,
logoutOAuth,
saveOAuthCredentials,
} from '../../utils/oauth.mts'
import { getEnterpriseOrgs, getOrgSlugs } from '../../utils/organization.mts'
import { setupSdk } from '../../utils/sdk.mts'
import {
getDefaultApiBaseUrl,
getDefaultProxyUrl,
setupSdk,
} from '../../utils/sdk.mts'
import { socketDocsLink } from '../../utils/terminal-link.mts'
import { setupTabCompletion } from '../install/setup-tab-completion.mts'
import { fetchOrganization } from '../organization/fetch-organization-list.mts'
Expand All @@ -29,9 +40,21 @@ type OrgChoices = Array<Separator | OrgChoice>
export async function attemptLogin(
apiBaseUrl: string | undefined,
apiProxy: string | undefined,
apiTokenMode = false,
) {
if (!apiTokenMode) {
return await attemptOAuthLogin(apiBaseUrl, apiProxy)
}

return await attemptApiTokenLogin(apiBaseUrl, apiProxy)
}

async function attemptApiTokenLogin(
apiBaseUrl: string | undefined,
apiProxy: string | undefined,
) {
apiBaseUrl ??= getConfigValueOrUndef(CONFIG_KEY_API_BASE_URL) ?? undefined
apiProxy ??= getConfigValueOrUndef(CONFIG_KEY_API_PROXY) ?? undefined
apiProxy ||= getDefaultProxyUrl()
const apiTokenInput = await password({
message: `Enter your ${socketDocsLink('/docs/api-keys', 'Socket.dev API token')} (leave blank to use a limited public token)`,
})
Expand Down Expand Up @@ -154,6 +177,18 @@ export async function attemptLogin(
const previousPersistedToken = getConfigValueOrUndef(CONFIG_KEY_API_TOKEN)
try {
applyLogin(apiToken, enforcedOrgs, apiBaseUrl, apiProxy)
if (constants.socketAppDataPath && !isConfigFromFlag()) {
try {
const result = await logoutOAuth(constants.socketAppDataPath, {
apiProxy,
})
if (result.warning) {
logger.warn(result.warning)
}
} catch {
logger.warn('The previous browser sign-in could not be removed.')
}
}
logger.success(
`API credentials ${previousPersistedToken === apiToken ? 'refreshed' : previousPersistedToken ? 'updated' : 'set'}`,
)
Expand All @@ -165,6 +200,109 @@ export async function attemptLogin(
}
} catch {
process.exitCode = 1
logger.fail(`API login failed`)
logger.fail('API login failed')
}
}

async function attemptOAuthLogin(
apiBaseUrl: string | undefined,
apiProxy: string | undefined,
) {
if (isConfigFromFlag()) {
throw new Error(
'Browser sign-in cannot be saved while --config or SOCKET_CLI_CONFIG is active. Remove the config override and run `socket login` again.',
)
}
const configuredBaseUrl = apiBaseUrl || getDefaultApiBaseUrl()
if (configuredBaseUrl) {
let configuredOrigin: string
try {
configuredOrigin = new URL(configuredBaseUrl).origin
} catch {
throw new Error('The configured Socket API URL is invalid for OAuth.')
}
if (configuredOrigin !== OAUTH_ISSUER) {
throw new Error(
'Browser sign-in is only available with Socket’s trusted API URL. Use `socket login --api-token --api-base-url <url>` for a custom API URL.',
)
}
}

const existingToken = getConfigValueOrUndef(CONFIG_KEY_API_TOKEN)
const activeAuthType = getConfigValueOrUndef(CONFIG_KEY_AUTH_TYPE)
if (existingToken && activeAuthType !== 'oauth') {
const choice = await select({
message:
'An API token is already saved. Keep using it or switch to read-only browser sign-in?',
choices: [
{ name: 'Keep the saved API token', value: 'keep' },
{
name: 'Switch to browser sign-in (read-only)',
value: 'oauth',
},
],
})
if (choice === undefined) {
logger.fail('Canceled by user')
return { ok: false, message: 'Canceled', cause: 'Canceled by user' }
}
if (choice === 'keep') {
logger.success('Kept the saved API token login')
return { ok: true }
}
}

apiProxy ||= getDefaultProxyUrl()
const result = await beginOAuthLogin({
apiProxy,
openBrowser: true,
onAuthorizationUrl(url) {
logger.log('Open this URL in your browser to sign in:')
logger.log(url)
},
Comment thread
jdalton marked this conversation as resolved.
Resolved
})
const baseUrl = new URL('/v0/', result.apiOrigin).href
const sdkResult = await setupSdk({
apiBaseUrl: baseUrl,
apiProxy,
apiToken: result.accessToken,
})
if (!sdkResult.ok) {
process.exitCode = 1
logger.fail(failMsgWithBadge(sdkResult.message, sdkResult.cause))
return
}
const orgsResult = await fetchOrganization({
description: 'browser sign-in validation',
sdk: sdkResult.data,
})
if (!orgsResult.ok) {
process.exitCode = 1
logger.fail(failMsgWithBadge(orgsResult.message, orgsResult.cause))
return
}
const orgSlugs = getOrgSlugs(orgsResult.data.organizations)
if (orgSlugs.length === 0) {
process.exitCode = 1
logger.fail(
'Browser sign-in succeeded, but no accessible organization was returned. Check your organization membership and try again; existing credentials were kept.',
)
return
}

await saveOAuthCredentials(constants.socketAppDataPath!, result)
updateConfigValue(CONFIG_KEY_API_TOKEN, null)
updateConfigValue(CONFIG_KEY_DEFAULT_ORG, orgSlugs[0])
updateConfigValue(CONFIG_KEY_API_BASE_URL, null)
updateConfigValue(CONFIG_KEY_API_PROXY, apiProxy)
updateConfigValue(CONFIG_KEY_AUTH_TYPE, 'oauth')
Comment thread
jdalton marked this conversation as resolved.
Resolved
logger.success(`Browser sign-in saved for ${joinAnd(orgSlugs)}`)
logger.warn(
'This sign-in is read-only and supports package, alert, and threat-feed queries. Write and scan commands require API-token credentials; use `socket login --api-token` or SOCKET_CLI_API_TOKEN.',
)
if (constants.ENV.SOCKET_CLI_API_TOKEN) {
logger.warn(
'SOCKET_CLI_API_TOKEN remains active and takes precedence over browser sign-in for this session.',
)
}
}
Loading
Loading