@@ -7,6 +7,7 @@ import constants, {
77 CONFIG_KEY_API_BASE_URL ,
88 CONFIG_KEY_API_PROXY ,
99 CONFIG_KEY_API_TOKEN ,
10+ CONFIG_KEY_AUTH_TYPE ,
1011 CONFIG_KEY_DEFAULT_ORG ,
1112} from '../../constants.mts'
1213import {
@@ -15,8 +16,18 @@ import {
1516 updateConfigValue ,
1617} from '../../utils/config.mts'
1718import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts'
19+ import {
20+ OAUTH_ISSUER ,
21+ beginOAuthLogin ,
22+ logoutOAuth ,
23+ saveOAuthCredentials ,
24+ } from '../../utils/oauth.mts'
1825import { getEnterpriseOrgs , getOrgSlugs } from '../../utils/organization.mts'
19- import { setupSdk } from '../../utils/sdk.mts'
26+ import {
27+ getDefaultApiBaseUrl ,
28+ getDefaultProxyUrl ,
29+ setupSdk ,
30+ } from '../../utils/sdk.mts'
2031import { socketDocsLink } from '../../utils/terminal-link.mts'
2132import { setupTabCompletion } from '../install/setup-tab-completion.mts'
2233import { fetchOrganization } from '../organization/fetch-organization-list.mts'
@@ -29,9 +40,21 @@ type OrgChoices = Array<Separator | OrgChoice>
2940export async function attemptLogin (
3041 apiBaseUrl : string | undefined ,
3142 apiProxy : string | undefined ,
43+ apiTokenMode = false ,
44+ ) {
45+ if ( ! apiTokenMode ) {
46+ return await attemptOAuthLogin ( apiBaseUrl , apiProxy )
47+ }
48+
49+ return await attemptApiTokenLogin ( apiBaseUrl , apiProxy )
50+ }
51+
52+ async function attemptApiTokenLogin (
53+ apiBaseUrl : string | undefined ,
54+ apiProxy : string | undefined ,
3255) {
3356 apiBaseUrl ??= getConfigValueOrUndef ( CONFIG_KEY_API_BASE_URL ) ?? undefined
34- apiProxy ??= getConfigValueOrUndef ( CONFIG_KEY_API_PROXY ) ?? undefined
57+ apiProxy ||= getDefaultProxyUrl ( )
3558 const apiTokenInput = await password ( {
3659 message : `Enter your ${ socketDocsLink ( '/docs/api-keys' , 'Socket.dev API token' ) } (leave blank to use a limited public token)` ,
3760 } )
@@ -154,6 +177,18 @@ export async function attemptLogin(
154177 const previousPersistedToken = getConfigValueOrUndef ( CONFIG_KEY_API_TOKEN )
155178 try {
156179 applyLogin ( apiToken , enforcedOrgs , apiBaseUrl , apiProxy )
180+ if ( constants . socketAppDataPath && ! isConfigFromFlag ( ) ) {
181+ try {
182+ const result = await logoutOAuth ( constants . socketAppDataPath , {
183+ apiProxy,
184+ } )
185+ if ( result . warning ) {
186+ logger . warn ( result . warning )
187+ }
188+ } catch {
189+ logger . warn ( 'The previous browser sign-in could not be removed.' )
190+ }
191+ }
157192 logger . success (
158193 `API credentials ${ previousPersistedToken === apiToken ? 'refreshed' : previousPersistedToken ? 'updated' : 'set' } ` ,
159194 )
@@ -165,6 +200,109 @@ export async function attemptLogin(
165200 }
166201 } catch {
167202 process . exitCode = 1
168- logger . fail ( `API login failed` )
203+ logger . fail ( 'API login failed' )
204+ }
205+ }
206+
207+ async function attemptOAuthLogin (
208+ apiBaseUrl : string | undefined ,
209+ apiProxy : string | undefined ,
210+ ) {
211+ if ( isConfigFromFlag ( ) ) {
212+ throw new Error (
213+ 'Browser sign-in cannot be saved while --config or SOCKET_CLI_CONFIG is active. Remove the config override and run `socket login` again.' ,
214+ )
215+ }
216+ const configuredBaseUrl = apiBaseUrl || getDefaultApiBaseUrl ( )
217+ if ( configuredBaseUrl ) {
218+ let configuredOrigin : string
219+ try {
220+ configuredOrigin = new URL ( configuredBaseUrl ) . origin
221+ } catch {
222+ throw new Error ( 'The configured Socket API URL is invalid for OAuth.' )
223+ }
224+ if ( configuredOrigin !== OAUTH_ISSUER ) {
225+ throw new Error (
226+ '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.' ,
227+ )
228+ }
229+ }
230+
231+ const existingToken = getConfigValueOrUndef ( CONFIG_KEY_API_TOKEN )
232+ const activeAuthType = getConfigValueOrUndef ( CONFIG_KEY_AUTH_TYPE )
233+ if ( existingToken && activeAuthType !== 'oauth' ) {
234+ const choice = await select ( {
235+ message :
236+ 'An API token is already saved. Keep using it or switch to read-only browser sign-in?' ,
237+ choices : [
238+ { name : 'Keep the saved API token' , value : 'keep' } ,
239+ {
240+ name : 'Switch to browser sign-in (read-only)' ,
241+ value : 'oauth' ,
242+ } ,
243+ ] ,
244+ } )
245+ if ( choice === undefined ) {
246+ logger . fail ( 'Canceled by user' )
247+ return { ok : false , message : 'Canceled' , cause : 'Canceled by user' }
248+ }
249+ if ( choice === 'keep' ) {
250+ logger . success ( 'Kept the saved API token login' )
251+ return { ok : true }
252+ }
253+ }
254+
255+ apiProxy ||= getDefaultProxyUrl ( )
256+ const result = await beginOAuthLogin ( {
257+ apiProxy,
258+ openBrowser : true ,
259+ onAuthorizationUrl ( url ) {
260+ logger . log ( 'Open this URL in your browser to sign in:' )
261+ logger . log ( url )
262+ } ,
263+ } )
264+ const baseUrl = new URL ( '/v0/' , result . apiOrigin ) . href
265+ const sdkResult = await setupSdk ( {
266+ apiBaseUrl : baseUrl ,
267+ apiProxy,
268+ apiToken : result . accessToken ,
269+ } )
270+ if ( ! sdkResult . ok ) {
271+ process . exitCode = 1
272+ logger . fail ( failMsgWithBadge ( sdkResult . message , sdkResult . cause ) )
273+ return
274+ }
275+ const orgsResult = await fetchOrganization ( {
276+ description : 'browser sign-in validation' ,
277+ sdk : sdkResult . data ,
278+ } )
279+ if ( ! orgsResult . ok ) {
280+ process . exitCode = 1
281+ logger . fail ( failMsgWithBadge ( orgsResult . message , orgsResult . cause ) )
282+ return
283+ }
284+ const orgSlugs = getOrgSlugs ( orgsResult . data . organizations )
285+ if ( orgSlugs . length === 0 ) {
286+ process . exitCode = 1
287+ logger . fail (
288+ 'Browser sign-in succeeded, but no accessible organization was returned. Check your organization membership and try again; existing credentials were kept.' ,
289+ )
290+ return
291+ }
292+
293+ await saveOAuthCredentials ( constants . socketAppDataPath ! , result )
294+ updateConfigValue ( CONFIG_KEY_API_TOKEN , null )
295+ updateConfigValue ( CONFIG_KEY_DEFAULT_ORG , orgSlugs [ 0 ] )
296+ updateConfigValue ( CONFIG_KEY_API_BASE_URL , null )
297+ updateConfigValue ( CONFIG_KEY_API_PROXY , apiProxy )
298+ updateConfigValue ( CONFIG_KEY_AUTH_TYPE , 'oauth' )
299+ logger . success ( `Browser sign-in saved for ${ joinAnd ( orgSlugs ) } ` )
300+ logger . warn (
301+ '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.' ,
302+ )
303+ if ( constants . ENV . SOCKET_CLI_API_TOKEN ) {
304+ logger . warn (
305+ 'SOCKET_CLI_API_TOKEN remains active and takes precedence over browser sign-in for this session.' ,
306+ )
169307 }
170308}
0 commit comments