@@ -2,19 +2,27 @@ import { joinAnd } from '@socketsecurity/lib/arrays'
22import { SOCKET_PUBLIC_API_TOKEN } from '@socketsecurity/lib/constants/socket'
33import { getDefaultLogger } from '@socketsecurity/lib/logger'
44import { confirm , password , select } from '@socketsecurity/lib/stdio/prompts'
5+ import { isNonEmptyString } from '@socketsecurity/lib/strings'
56
67import { applyLogin } from './apply-login.mts'
8+ import { oauthLogin } from './oauth-login.mts'
79import {
810 CONFIG_KEY_API_BASE_URL ,
911 CONFIG_KEY_API_PROXY ,
1012 CONFIG_KEY_API_TOKEN ,
13+ CONFIG_KEY_AUTH_BASE_URL ,
1114 CONFIG_KEY_DEFAULT_ORG ,
15+ CONFIG_KEY_OAUTH_CLIENT_ID ,
16+ CONFIG_KEY_OAUTH_REDIRECT_URI ,
17+ CONFIG_KEY_OAUTH_SCOPES ,
1218} from '../../constants/config.mts'
19+ import ENV from '../../constants/env.mts'
1320import {
1421 getConfigValueOrUndef ,
1522 isConfigFromFlag ,
1623 updateConfigValue ,
1724} from '../../utils/config.mts'
25+ import { deriveAuthBaseUrlFromApiBaseUrl } from '../../utils/auth/oauth.mts'
1826import { failMsgWithBadge } from '../../utils/error/fail-msg-with-badge.mts'
1927import { getEnterpriseOrgs , getOrgSlugs } from '../../utils/organization.mts'
2028import { setupSdk } from '../../utils/socket/sdk.mjs'
@@ -23,27 +31,150 @@ import { setupTabCompletion } from '../install/setup-tab-completion.mts'
2331import { fetchOrganization } from '../organization/fetch-organization-list.mts'
2432
2533import type { Choice } from '@socketsecurity/lib/stdio/prompts'
34+ import requirements from '../../../data/command-api-requirements.json' with {
35+ type : 'json' ,
36+ }
2637const logger = getDefaultLogger ( )
2738
2839type OrgChoice = Choice < string >
2940type OrgChoices = OrgChoice [ ]
3041
42+ type LoginMethod = 'oauth' | 'token'
43+
44+ function getDefaultOAuthScopes ( ) : string [ ] {
45+ const permissions : string [ ] = [ ]
46+ const api = ( requirements as any ) ?. api ?? { }
47+ for ( const value of Object . values ( api ) as any [ ] ) {
48+ const perms = ( value ?. permissions ?? [ ] ) as unknown
49+ if ( Array . isArray ( perms ) ) {
50+ for ( const p of perms ) {
51+ if ( typeof p === 'string' && p ) {
52+ permissions . push ( p )
53+ }
54+ }
55+ }
56+ }
57+ return [ ...new Set ( permissions ) ] . sort ( )
58+ }
59+
60+ function parseScopes ( value : unknown ) : string [ ] | undefined {
61+ if ( Array . isArray ( value ) ) {
62+ return value
63+ . filter ( ( v ) : v is string => typeof v === 'string' && v . length > 0 )
64+ . sort ( )
65+ }
66+ if ( ! isNonEmptyString ( String ( value ?? '' ) ) ) {
67+ return undefined
68+ }
69+ const raw = String ( value )
70+ return raw
71+ . split ( / [ , \s ] + / u)
72+ . map ( s => s . trim ( ) )
73+ . filter ( Boolean )
74+ }
75+
3176export async function attemptLogin (
3277 apiBaseUrl : string | undefined ,
3378 apiProxy : string | undefined ,
79+ options ?: {
80+ method ?: LoginMethod | undefined
81+ authBaseUrl ?: string | undefined
82+ oauthClientId ?: string | undefined
83+ oauthRedirectUri ?: string | undefined
84+ oauthScopes ?: string | undefined
85+ } ,
3486) {
3587 apiBaseUrl ??= getConfigValueOrUndef ( CONFIG_KEY_API_BASE_URL ) ?? undefined
3688 apiProxy ??= getConfigValueOrUndef ( CONFIG_KEY_API_PROXY ) ?? undefined
37- const apiTokenInput = await password ( {
38- message : `Enter your ${ socketDocsLink ( '/docs/api-keys' , 'Socket.dev API token' ) } (leave blank to use a limited public token)` ,
39- } )
89+ const method : LoginMethod = options ?. method ?? 'oauth'
4090
41- if ( apiTokenInput === undefined ) {
42- logger . fail ( 'Canceled by user' )
43- return { ok : false , message : 'Canceled' , cause : 'Canceled by user' }
44- }
91+ let apiToken : string
92+ let oauthRefreshToken : string | null | undefined
93+ let oauthTokenExpiresAt : number | null | undefined
94+ let authBaseUrl : string | null | undefined
95+ let oauthClientId : string | null | undefined
96+ let oauthRedirectUri : string | null | undefined
97+ let oauthScopes : string [ ] | null | undefined
4598
46- const apiToken = apiTokenInput || SOCKET_PUBLIC_API_TOKEN
99+ if ( method === 'token' ) {
100+ const apiTokenInput = await password ( {
101+ message : `Enter your ${ socketDocsLink ( '/docs/api-keys' , 'Socket.dev API token' ) } (leave blank to use a limited public token)` ,
102+ } )
103+
104+ if ( apiTokenInput === undefined ) {
105+ logger . fail ( 'Canceled by user' )
106+ return { ok : false , message : 'Canceled' , cause : 'Canceled by user' }
107+ }
108+
109+ apiToken = apiTokenInput || SOCKET_PUBLIC_API_TOKEN
110+
111+ // Explicitly disable OAuth refresh flow when using a legacy org-wide token.
112+ oauthRefreshToken = null
113+ oauthTokenExpiresAt = null
114+ authBaseUrl = null
115+ oauthClientId = null
116+ oauthRedirectUri = null
117+ oauthScopes = null
118+ } else {
119+ const resolvedAuthBaseUrl =
120+ options ?. authBaseUrl ||
121+ ENV . SOCKET_CLI_AUTH_BASE_URL ||
122+ getConfigValueOrUndef ( CONFIG_KEY_AUTH_BASE_URL ) ||
123+ deriveAuthBaseUrlFromApiBaseUrl ( apiBaseUrl )
124+
125+ if ( ! isNonEmptyString ( resolvedAuthBaseUrl ) ) {
126+ process . exitCode = 1
127+ logger . fail (
128+ 'OAuth auth base URL is not configured. Provide --auth-base-url or set SOCKET_CLI_AUTH_BASE_URL.' ,
129+ )
130+ return
131+ }
132+
133+ const resolvedClientId =
134+ options ?. oauthClientId ||
135+ ENV . SOCKET_CLI_OAUTH_CLIENT_ID ||
136+ getConfigValueOrUndef ( CONFIG_KEY_OAUTH_CLIENT_ID ) ||
137+ 'socket-cli'
138+
139+ const resolvedRedirectUri =
140+ options ?. oauthRedirectUri ||
141+ ENV . SOCKET_CLI_OAUTH_REDIRECT_URI ||
142+ getConfigValueOrUndef ( CONFIG_KEY_OAUTH_REDIRECT_URI ) ||
143+ 'http://127.0.0.1:53682/callback'
144+
145+ const resolvedScopes =
146+ parseScopes (
147+ options ?. oauthScopes ||
148+ ENV . SOCKET_CLI_OAUTH_SCOPES ||
149+ getConfigValueOrUndef ( CONFIG_KEY_OAUTH_SCOPES ) ||
150+ getDefaultOAuthScopes ( ) ,
151+ ) ?? [ ]
152+
153+ logger . log (
154+ `Opening your browser to complete login (client_id: ${ resolvedClientId } )...` ,
155+ )
156+
157+ const oauthResult = await oauthLogin ( {
158+ authBaseUrl : resolvedAuthBaseUrl ,
159+ clientId : resolvedClientId ,
160+ redirectUri : resolvedRedirectUri ,
161+ scopes : resolvedScopes ,
162+ apiProxy,
163+ } )
164+ if ( ! oauthResult . ok ) {
165+ process . exitCode = 1
166+ logger . fail ( failMsgWithBadge ( oauthResult . message , oauthResult . cause ) )
167+ return
168+ }
169+
170+ apiToken = oauthResult . data . accessToken
171+ oauthRefreshToken = oauthResult . data . refreshToken
172+ oauthTokenExpiresAt = oauthResult . data . expiresAt
173+ authBaseUrl = resolvedAuthBaseUrl
174+ oauthClientId = resolvedClientId
175+ oauthRedirectUri = resolvedRedirectUri
176+ oauthScopes = resolvedScopes
177+ }
47178
48179 const sockSdkCResult = await setupSdk ( { apiBaseUrl, apiProxy, apiToken } )
49180 if ( ! sockSdkCResult . ok ) {
@@ -155,7 +286,18 @@ export async function attemptLogin(
155286
156287 const previousPersistedToken = getConfigValueOrUndef ( CONFIG_KEY_API_TOKEN )
157288 try {
158- applyLogin ( apiToken , enforcedOrgs , apiBaseUrl , apiProxy )
289+ applyLogin ( {
290+ apiToken,
291+ enforcedOrgs,
292+ apiBaseUrl,
293+ apiProxy,
294+ authBaseUrl,
295+ oauthClientId,
296+ oauthRedirectUri,
297+ oauthRefreshToken,
298+ oauthScopes,
299+ oauthTokenExpiresAt,
300+ } )
159301 logger . success (
160302 `API credentials ${ previousPersistedToken === apiToken ? 'refreshed' : previousPersistedToken ? 'updated' : 'set' } ` ,
161303 )
0 commit comments