From 9d182daf10eeccf1a1db795e8c5f664975f08748 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Mon, 12 May 2025 13:18:41 -0300 Subject: [PATCH 1/9] feat: `org login web` support for multi auth --- messages/web.login.md | 8 ++++++ src/commands/org/login/web.ts | 46 +++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/messages/web.login.md b/messages/web.login.md index 1176f1c7..1c4eb7f5 100644 --- a/messages/web.login.md +++ b/messages/web.login.md @@ -42,6 +42,14 @@ Browser in which to open the org. If you don’t specify --browser, the command uses your default browser. The exact names of the browser applications differ depending on the operating system you're on; check your documentation for details. +# flags.app.summary + +Name of the connected app or external client app. + +# flags.username.summary + +Username of the user logging in. + # deviceWarning "<%= config.bin %> <%= command.id %>" doesn't work when authorizing to a headless environment. Use "<%= config.bin %> org login device" instead. diff --git a/src/commands/org/login/web.ts b/src/commands/org/login/web.ts index a64822d9..208dda38 100644 --- a/src/commands/org/login/web.ts +++ b/src/commands/org/login/web.ts @@ -69,6 +69,15 @@ export default class LoginWeb extends SfCommand { aliases: ['noprompt'], }), loglevel, + // TODO: flag name is too generic, rename before final review + app: Flags.string({ + summary: messages.getMessage('flags.app.summary'), + dependsOn: ['username'], + }), + username: Flags.string({ + summary: messages.getMessage('flags.username.summary'), + dependsOn: ['app'], + }), }; private logger = Logger.childFromRoot(this.constructor.name); @@ -81,6 +90,28 @@ export default class LoginWeb extends SfCommand { if (await common.shouldExitCommand(flags['no-prompt'])) return {}; + // Add ca/eca to already existing auth info. + if (flags.app && flags.username) { + // 1. get username authinfo + const userAuthInfo = await AuthInfo.create({ + username: flags.username, + }); + + const authFields = userAuthInfo.getFields(true); + + // 2. web-auth and save name, clientId, accessToken, and refreshToken in `apps` object + const oauthConfig: OAuth2Config = { + // TODO: handle clientSecret prompt + loginUrl: authFields.loginUrl, + clientId: flags['client-id'], + }; + + await this.executeLoginFlow(oauthConfig, flags.browser, flags.app, flags.username); + + // TODO: add successful app auth msg + return userAuthInfo.getFields(true); + } + const oauthConfig: OAuth2Config = { loginUrl: await common.resolveLoginUrl(flags['instance-url']?.href), clientId: flags['client-id'], @@ -113,8 +144,19 @@ export default class LoginWeb extends SfCommand { // leave it because it's stubbed in the test // eslint-disable-next-line class-methods-use-this - private async executeLoginFlow(oauthConfig: OAuth2Config, browser?: string): Promise { - const oauthServer = await WebOAuthServer.create({ oauthConfig }); + private async executeLoginFlow( + oauthConfig: OAuth2Config, + browser?: string, + // TODO: rename + capp?: string, + username?: string + ): Promise { + // TODO: document how this works: + // 1st case: new user, server creates auth file with new creds + // 2nd case: existing auth file, server gets tokens and adds it to the file + // + // WebOAuthServer types should block any other combination of object params. + const oauthServer = await WebOAuthServer.create({ oauthConfig, app: capp, username }); await oauthServer.start(); const app = browser && browser in apps ? (browser as AppName) : undefined; const openOptions = app ? { app: { name: apps[app] }, wait: false } : { wait: false }; From bb20329eadb1dea361dc8f6a7ad992c55438c8b1 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 6 Jun 2025 13:41:45 -0300 Subject: [PATCH 2/9] chore: add scopes flag --- command-snapshot.json | 5 ++++- messages/web.login.md | 4 ++++ src/commands/org/login/web.ts | 42 +++++++++++++++++++++-------------- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/command-snapshot.json b/command-snapshot.json index 1c5140eb..82332969 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -131,6 +131,7 @@ "flagChars": ["a", "b", "d", "i", "p", "r", "s"], "flags": [ "alias", + "app", "browser", "client-id", "flags-dir", @@ -138,8 +139,10 @@ "json", "loglevel", "no-prompt", + "scopes", "set-default", - "set-default-dev-hub" + "set-default-dev-hub", + "username" ], "plugin": "@salesforce/plugin-auth" }, diff --git a/messages/web.login.md b/messages/web.login.md index 1c4eb7f5..fe195004 100644 --- a/messages/web.login.md +++ b/messages/web.login.md @@ -50,6 +50,10 @@ Name of the connected app or external client app. Username of the user logging in. +# flags.scopes.summary + +Authentication scopes to request. + # deviceWarning "<%= config.bin %> <%= command.id %>" doesn't work when authorizing to a headless environment. Use "<%= config.bin %> org login device" instead. diff --git a/src/commands/org/login/web.ts b/src/commands/org/login/web.ts index 208dda38..eb28950a 100644 --- a/src/commands/org/login/web.ts +++ b/src/commands/org/login/web.ts @@ -69,7 +69,6 @@ export default class LoginWeb extends SfCommand { aliases: ['noprompt'], }), loglevel, - // TODO: flag name is too generic, rename before final review app: Flags.string({ summary: messages.getMessage('flags.app.summary'), dependsOn: ['username'], @@ -78,6 +77,9 @@ export default class LoginWeb extends SfCommand { summary: messages.getMessage('flags.username.summary'), dependsOn: ['app'], }), + scopes: Flags.string({ + summary: messages.getMessage('flags.scopes.summary'), + }), }; private logger = Logger.childFromRoot(this.constructor.name); @@ -104,9 +106,10 @@ export default class LoginWeb extends SfCommand { // TODO: handle clientSecret prompt loginUrl: authFields.loginUrl, clientId: flags['client-id'], + ...{ clientSecret: await this.secretPrompt({ message: commonMessages.getMessage('clientSecretStdin') }) }, }; - await this.executeLoginFlow(oauthConfig, flags.browser, flags.app, flags.username); + await this.executeLoginFlow(oauthConfig, flags.browser, flags.app, flags.username, flags.scopes); // TODO: add successful app auth msg return userAuthInfo.getFields(true); @@ -147,20 +150,25 @@ export default class LoginWeb extends SfCommand { private async executeLoginFlow( oauthConfig: OAuth2Config, browser?: string, - // TODO: rename - capp?: string, - username?: string + app?: string, + username?: string, + scopes?: string ): Promise { - // TODO: document how this works: - // 1st case: new user, server creates auth file with new creds - // 2nd case: existing auth file, server gets tokens and adds it to the file - // - // WebOAuthServer types should block any other combination of object params. - const oauthServer = await WebOAuthServer.create({ oauthConfig, app: capp, username }); + // The server handles 2 possible auth scenarios: + // a. 1st time auth, creates auth file. + // b. Add CA/ECA to existing auth. + const oauthServer = await WebOAuthServer.create({ + oauthConfig: { + ...oauthConfig, + scope: scopes, + }, + app, + username, + }); await oauthServer.start(); - const app = browser && browser in apps ? (browser as AppName) : undefined; - const openOptions = app ? { app: { name: apps[app] }, wait: false } : { wait: false }; - this.logger.debug(`Opening browser ${app ?? ''}`); + const browserApp = browser && browser in apps ? (browser as AppName) : undefined; + const openOptions = browserApp ? { app: { name: apps[browserApp] }, wait: false } : { wait: false }; + this.logger.debug(`Opening browser ${browserApp ?? ''}`); // the following `childProcess` wrapper is needed to catch when `open` fails to open a browser. await open(oauthServer.getAuthorizationUrl(), openOptions).then( (childProcess) => @@ -168,13 +176,13 @@ export default class LoginWeb extends SfCommand { // https://nodejs.org/api/child_process.html#event-exit childProcess.on('exit', (code) => { if (code && code > 0) { - this.logger.debug(`Failed to open browser ${app ?? ''}`); - reject(messages.createError('error.cannotOpenBrowser', [app], [app])); + this.logger.debug(`Failed to open browser ${browserApp ?? ''}`); + reject(messages.createError('error.cannotOpenBrowser', [browserApp], [browserApp])); } // If the process exited, code is the final exit code of the process, otherwise null. // resolve on null just to be safe, worst case the browser didn't open and the CLI just hangs. if (code === null || code === 0) { - this.logger.debug(`Successfully opened browser ${app ?? ''}`); + this.logger.debug(`Successfully opened browser ${browserApp ?? ''}`); resolve(childProcess); } }); From 16f8865f0770c557a3199b3c4e780a19befb2474 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 6 Jun 2025 13:43:03 -0300 Subject: [PATCH 3/9] chore: update schemas --- schemas/org-login-access__token.json | 26 ++++++++++++++++++++++++++ schemas/org-login-device.json | 26 ++++++++++++++++++++++++++ schemas/org-login-jwt.json | 26 ++++++++++++++++++++++++++ schemas/org-login-sfdx__url.json | 26 ++++++++++++++++++++++++++ schemas/org-login-web.json | 26 ++++++++++++++++++++++++++ 5 files changed, 130 insertions(+) diff --git a/schemas/org-login-access__token.json b/schemas/org-login-access__token.json index 23f3c1da..9c3b5fb9 100644 --- a/schemas/org-login-access__token.json +++ b/schemas/org-login-access__token.json @@ -5,6 +5,32 @@ "AuthFields": { "type": "object", "properties": { + "apps": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "oauthFlow": { + "type": "string", + "const": "web" + } + }, + "required": ["clientId", "accessToken", "refreshToken", "oauthFlow"], + "additionalProperties": false + } + }, "accessToken": { "type": "string" }, diff --git a/schemas/org-login-device.json b/schemas/org-login-device.json index a3c447be..58d20442 100644 --- a/schemas/org-login-device.json +++ b/schemas/org-login-device.json @@ -22,6 +22,32 @@ "verification_uri": { "type": "string" }, + "apps": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "oauthFlow": { + "type": "string", + "const": "web" + } + }, + "required": ["clientId", "accessToken", "refreshToken", "oauthFlow"], + "additionalProperties": false + } + }, "accessToken": { "type": "string" }, diff --git a/schemas/org-login-jwt.json b/schemas/org-login-jwt.json index 23f3c1da..9c3b5fb9 100644 --- a/schemas/org-login-jwt.json +++ b/schemas/org-login-jwt.json @@ -5,6 +5,32 @@ "AuthFields": { "type": "object", "properties": { + "apps": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "oauthFlow": { + "type": "string", + "const": "web" + } + }, + "required": ["clientId", "accessToken", "refreshToken", "oauthFlow"], + "additionalProperties": false + } + }, "accessToken": { "type": "string" }, diff --git a/schemas/org-login-sfdx__url.json b/schemas/org-login-sfdx__url.json index 23f3c1da..9c3b5fb9 100644 --- a/schemas/org-login-sfdx__url.json +++ b/schemas/org-login-sfdx__url.json @@ -5,6 +5,32 @@ "AuthFields": { "type": "object", "properties": { + "apps": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "oauthFlow": { + "type": "string", + "const": "web" + } + }, + "required": ["clientId", "accessToken", "refreshToken", "oauthFlow"], + "additionalProperties": false + } + }, "accessToken": { "type": "string" }, diff --git a/schemas/org-login-web.json b/schemas/org-login-web.json index 23f3c1da..9c3b5fb9 100644 --- a/schemas/org-login-web.json +++ b/schemas/org-login-web.json @@ -5,6 +5,32 @@ "AuthFields": { "type": "object", "properties": { + "apps": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "oauthFlow": { + "type": "string", + "const": "web" + } + }, + "required": ["clientId", "accessToken", "refreshToken", "oauthFlow"], + "additionalProperties": false + } + }, "accessToken": { "type": "string" }, From acb7d6fa8a78000af6a3fa1215fb928ea5fedfb2 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 6 Jun 2025 16:09:37 -0300 Subject: [PATCH 4/9] chore: bump core --- package.json | 2 +- test/commands/org/login/login.jwt.test.ts | 4 +-- yarn.lock | 42 ++++++++++++++--------- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index bd46bf7a..94a671a5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@inquirer/checkbox": "^2.5.0", "@inquirer/select": "^2.5.0", "@oclif/core": "^4", - "@salesforce/core": "^8.11.4", + "@salesforce/core": "^8.12.1-dev.0", "@salesforce/kit": "^3.2.3", "@salesforce/plugin-info": "^3.4.65", "@salesforce/sf-plugins-core": "^12.2.2", diff --git a/test/commands/org/login/login.jwt.test.ts b/test/commands/org/login/login.jwt.test.ts index 7eda80a1..6f1b061b 100644 --- a/test/commands/org/login/login.jwt.test.ts +++ b/test/commands/org/login/login.jwt.test.ts @@ -178,13 +178,13 @@ describe('org:login:jwt', () => { ]); }); - it('should throw an error when client id is invalid', async () => { + it.only('should throw an error when client id is invalid', async () => { await prepareStubs({ authInfoCreateFails: true }); try { await LoginJwt.run(['-u', testData.username, '-f', 'path/to/key.json', '-i', '123456INVALID', '--json']); expect.fail('Should have thrown an error'); } catch (e) { - expect(e).to.be.instanceOf(SfError); + expect(e).to.be.instanceOf(Error); const jwtAuthError = e as SfError; expect(jwtAuthError.message).to.include('We encountered a JSON web token error'); expect(jwtAuthError.message).to.include('invalid client id'); diff --git a/yarn.lock b/yarn.lock index 456df53d..05cb57a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1688,7 +1688,7 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.1" -"@salesforce/core@^8.10.0", "@salesforce/core@^8.11.0", "@salesforce/core@^8.11.1", "@salesforce/core@^8.11.2", "@salesforce/core@^8.11.4", "@salesforce/core@^8.5.1", "@salesforce/core@^8.8.0": +"@salesforce/core@^8.10.0", "@salesforce/core@^8.11.0", "@salesforce/core@^8.11.1", "@salesforce/core@^8.11.2", "@salesforce/core@^8.5.1", "@salesforce/core@^8.8.0": version "8.11.4" resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.11.4.tgz#e4f049351540a46e12bb5c1b6574232fd4d1288b" integrity sha512-6jrACrCmpic7mrnp4XQ6tiyx5FvHs101dQ2v+m8+aHF97036bul+GeeYuSjVp3ASh0sjR5CotYf7R65chd4H+A== @@ -1712,6 +1712,30 @@ semver "^7.6.3" ts-retry-promise "^0.8.1" +"@salesforce/core@^8.12.1-dev.0": + version "8.12.1-dev.0" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.12.1-dev.0.tgz#bb5ba0cb3f189496ce2f67ec88291c96a0255ccb" + integrity sha512-2tF0Qn33F31WD3jnW9Ve6tXoSY2+CxrBscaixMkoHDRvKsEB6aLxJ5fJImTx1/Awen3bJY9td0xjTIehLfdmGw== + dependencies: + "@jsforce/jsforce-node" "^3.8.2" + "@salesforce/kit" "^3.2.2" + "@salesforce/schemas" "^1.9.0" + "@salesforce/ts-types" "^2.0.10" + ajv "^8.17.1" + change-case "^4.1.2" + fast-levenshtein "^3.0.0" + faye "^1.4.0" + form-data "^4.0.0" + js2xmlparser "^4.0.1" + jsonwebtoken "9.0.2" + jszip "3.10.1" + pino "^9.7.0" + pino-abstract-transport "^1.2.0" + pino-pretty "^11.3.0" + proper-lockfile "^4.1.2" + semver "^7.6.3" + ts-retry-promise "^0.8.1" + "@salesforce/dev-config@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@salesforce/dev-config/-/dev-config-4.3.1.tgz#4dac8245df79d675258b50e1d24e8c636eaa5e10" @@ -2248,21 +2272,7 @@ "@smithy/types" "^4.2.0" tslib "^2.6.2" -"@smithy/signature-v4@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.0.2.tgz#363854e946fbc5bc206ff82e79ada5d5c14be640" - integrity sha512-Mz+mc7okA73Lyz8zQKJNyr7lIcHLiPYp0+oiqiMNc/t7/Kf2BENs5d63pEj7oPqdjaum6g0Fc8wC78dY1TgtXw== - dependencies: - "@smithy/is-array-buffer" "^4.0.0" - "@smithy/protocol-http" "^5.1.0" - "@smithy/types" "^4.2.0" - "@smithy/util-hex-encoding" "^4.0.0" - "@smithy/util-middleware" "^4.0.2" - "@smithy/util-uri-escape" "^4.0.0" - "@smithy/util-utf8" "^4.0.0" - tslib "^2.6.2" - -"@smithy/signature-v4@^5.1.0": +"@smithy/signature-v4@^5.0.2", "@smithy/signature-v4@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.1.0.tgz#2c56e5b278482b04383d84ea2c07b7f0a8eb8f63" integrity sha512-4t5WX60sL3zGJF/CtZsUQTs3UrZEDO2P7pEaElrekbLqkWPYkgqNW1oeiNYC6xXifBnT9dVBOnNQRvOE9riU9w== From 7fed787bde6f21649ca292b66c969c34462dbd62 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Mon, 9 Jun 2025 12:55:10 -0300 Subject: [PATCH 5/9] chore: review --- messages/web.login.md | 4 ++++ src/commands/org/login/web.ts | 3 +-- test/commands/org/login/login.jwt.test.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/messages/web.login.md b/messages/web.login.md index fe195004..1deeb0be 100644 --- a/messages/web.login.md +++ b/messages/web.login.md @@ -54,6 +54,10 @@ Username of the user logging in. Authentication scopes to request. +# linkedApp + +Successfully linked "%s" app to %s. + # deviceWarning "<%= config.bin %> <%= command.id %>" doesn't work when authorizing to a headless environment. Use "<%= config.bin %> org login device" instead. diff --git a/src/commands/org/login/web.ts b/src/commands/org/login/web.ts index eb28950a..473dae65 100644 --- a/src/commands/org/login/web.ts +++ b/src/commands/org/login/web.ts @@ -103,7 +103,6 @@ export default class LoginWeb extends SfCommand { // 2. web-auth and save name, clientId, accessToken, and refreshToken in `apps` object const oauthConfig: OAuth2Config = { - // TODO: handle clientSecret prompt loginUrl: authFields.loginUrl, clientId: flags['client-id'], ...{ clientSecret: await this.secretPrompt({ message: commonMessages.getMessage('clientSecretStdin') }) }, @@ -111,7 +110,7 @@ export default class LoginWeb extends SfCommand { await this.executeLoginFlow(oauthConfig, flags.browser, flags.app, flags.username, flags.scopes); - // TODO: add successful app auth msg + this.logSuccess(messages.getMessage('linkedApp', [flags.app, flags.username])); return userAuthInfo.getFields(true); } diff --git a/test/commands/org/login/login.jwt.test.ts b/test/commands/org/login/login.jwt.test.ts index 6f1b061b..66fd49c5 100644 --- a/test/commands/org/login/login.jwt.test.ts +++ b/test/commands/org/login/login.jwt.test.ts @@ -178,7 +178,7 @@ describe('org:login:jwt', () => { ]); }); - it.only('should throw an error when client id is invalid', async () => { + it('should throw an error when client id is invalid', async () => { await prepareStubs({ authInfoCreateFails: true }); try { await LoginJwt.run(['-u', testData.username, '-f', 'path/to/key.json', '-i', '123456INVALID', '--json']); From 7d3400ae127fe74cdf09b963ace39ce7cb365654 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Tue, 10 Jun 2025 18:27:04 -0300 Subject: [PATCH 6/9] chore: rename apps -> clientApps --- command-snapshot.json | 4 ++-- messages/web.login.md | 10 +++++----- schemas/org-login-access__token.json | 2 +- schemas/org-login-device.json | 2 +- schemas/org-login-jwt.json | 2 +- schemas/org-login-sfdx__url.json | 2 +- schemas/org-login-web.json | 2 +- src/commands/org/login/web.ts | 15 ++++++++------- 8 files changed, 20 insertions(+), 19 deletions(-) diff --git a/command-snapshot.json b/command-snapshot.json index 82332969..d78c5c76 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -128,11 +128,11 @@ "setdefaultusername", "v" ], - "flagChars": ["a", "b", "d", "i", "p", "r", "s"], + "flagChars": ["a", "b", "c", "d", "i", "p", "r", "s"], "flags": [ "alias", - "app", "browser", + "client-app", "client-id", "flags-dir", "instance-url", diff --git a/messages/web.login.md b/messages/web.login.md index 1deeb0be..b22cdfbd 100644 --- a/messages/web.login.md +++ b/messages/web.login.md @@ -42,21 +42,21 @@ Browser in which to open the org. If you don’t specify --browser, the command uses your default browser. The exact names of the browser applications differ depending on the operating system you're on; check your documentation for details. -# flags.app.summary +# flags.client-app.summary -Name of the connected app or external client app. +Name of the connected app or external client app to link to the user. # flags.username.summary -Username of the user logging in. +Username to link client app to. # flags.scopes.summary Authentication scopes to request. -# linkedApp +# linkedClientApp -Successfully linked "%s" app to %s. +Successfully linked "%s" client app to %s. # deviceWarning diff --git a/schemas/org-login-access__token.json b/schemas/org-login-access__token.json index 9c3b5fb9..e7d10ce0 100644 --- a/schemas/org-login-access__token.json +++ b/schemas/org-login-access__token.json @@ -5,7 +5,7 @@ "AuthFields": { "type": "object", "properties": { - "apps": { + "clientApps": { "type": "object", "additionalProperties": { "type": "object", diff --git a/schemas/org-login-device.json b/schemas/org-login-device.json index 58d20442..160350f6 100644 --- a/schemas/org-login-device.json +++ b/schemas/org-login-device.json @@ -22,7 +22,7 @@ "verification_uri": { "type": "string" }, - "apps": { + "clientApps": { "type": "object", "additionalProperties": { "type": "object", diff --git a/schemas/org-login-jwt.json b/schemas/org-login-jwt.json index 9c3b5fb9..e7d10ce0 100644 --- a/schemas/org-login-jwt.json +++ b/schemas/org-login-jwt.json @@ -5,7 +5,7 @@ "AuthFields": { "type": "object", "properties": { - "apps": { + "clientApps": { "type": "object", "additionalProperties": { "type": "object", diff --git a/schemas/org-login-sfdx__url.json b/schemas/org-login-sfdx__url.json index 9c3b5fb9..e7d10ce0 100644 --- a/schemas/org-login-sfdx__url.json +++ b/schemas/org-login-sfdx__url.json @@ -5,7 +5,7 @@ "AuthFields": { "type": "object", "properties": { - "apps": { + "clientApps": { "type": "object", "additionalProperties": { "type": "object", diff --git a/schemas/org-login-web.json b/schemas/org-login-web.json index 9c3b5fb9..e7d10ce0 100644 --- a/schemas/org-login-web.json +++ b/schemas/org-login-web.json @@ -5,7 +5,7 @@ "AuthFields": { "type": "object", "properties": { - "apps": { + "clientApps": { "type": "object", "additionalProperties": { "type": "object", diff --git a/src/commands/org/login/web.ts b/src/commands/org/login/web.ts index 473dae65..6a80115c 100644 --- a/src/commands/org/login/web.ts +++ b/src/commands/org/login/web.ts @@ -69,13 +69,14 @@ export default class LoginWeb extends SfCommand { aliases: ['noprompt'], }), loglevel, - app: Flags.string({ - summary: messages.getMessage('flags.app.summary'), + 'client-app': Flags.string({ + char: 'c', + summary: messages.getMessage('flags.client-app.summary'), dependsOn: ['username'], }), username: Flags.string({ summary: messages.getMessage('flags.username.summary'), - dependsOn: ['app'], + dependsOn: ['client-app'], }), scopes: Flags.string({ summary: messages.getMessage('flags.scopes.summary'), @@ -93,7 +94,7 @@ export default class LoginWeb extends SfCommand { if (await common.shouldExitCommand(flags['no-prompt'])) return {}; // Add ca/eca to already existing auth info. - if (flags.app && flags.username) { + if (flags['client-app'] && flags.username) { // 1. get username authinfo const userAuthInfo = await AuthInfo.create({ username: flags.username, @@ -108,9 +109,9 @@ export default class LoginWeb extends SfCommand { ...{ clientSecret: await this.secretPrompt({ message: commonMessages.getMessage('clientSecretStdin') }) }, }; - await this.executeLoginFlow(oauthConfig, flags.browser, flags.app, flags.username, flags.scopes); + await this.executeLoginFlow(oauthConfig, flags.browser, flags['client-app'], flags.username, flags.scopes); - this.logSuccess(messages.getMessage('linkedApp', [flags.app, flags.username])); + this.logSuccess(messages.getMessage('linkedClientApp', [flags['client-app'], flags.username])); return userAuthInfo.getFields(true); } @@ -161,7 +162,7 @@ export default class LoginWeb extends SfCommand { ...oauthConfig, scope: scopes, }, - app, + clientApp: app, username, }); await oauthServer.start(); From 58df65c706e8a51eae65c57f5efc380f6031c70d Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Wed, 11 Jun 2025 16:50:28 -0300 Subject: [PATCH 7/9] chore: logout --- command-snapshot.json | 4 ++-- messages/logout.md | 18 +++++++++++++++- src/commands/org/logout.ts | 42 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/command-snapshot.json b/command-snapshot.json index d78c5c76..b56050f2 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -150,8 +150,8 @@ "alias": ["force:auth:logout", "auth:logout"], "command": "org:logout", "flagAliases": ["noprompt", "targetusername", "u"], - "flagChars": ["a", "o", "p"], - "flags": ["all", "flags-dir", "json", "loglevel", "no-prompt", "target-org"], + "flagChars": ["a", "c", "o", "p"], + "flags": ["all", "client-app", "flags-dir", "json", "loglevel", "no-prompt", "target-org"], "plugin": "@salesforce/plugin-auth" } ] diff --git a/messages/logout.md b/messages/logout.md index 293baaf8..5065d022 100644 --- a/messages/logout.md +++ b/messages/logout.md @@ -40,10 +40,26 @@ Include all authenticated orgs. All orgs includes Dev Hubs, sandboxes, DE orgs, and expired, deleted, and unknown-status scratch orgs. +# flags.client-app.summary + +Client app to log out of. + # logoutOrgCommandSuccess Successfully logged out of orgs: %s +# logoutClientAppSuccess + +Successfully logged out of "%s" client app for user %s. + +# error.noLinkedApps + +%s does not have any client app linked. + +# error.invalidClientApp + +%s does not have a client app named "%s" linked. + # noOrgsFound No orgs found to log out of. @@ -82,6 +98,6 @@ You must specify a target-org (or default target-org config is set) or use --all # warning.NoAuthFoundForTargetOrg -No authenticated org found with the %s username or alias. +No authenticated org found with the %s username or alias. NOTE: Starting September 2025, this warning will be converted to an error. As a result, the exit code when you try to log out of an unauthenticated org will change from 0 to 1. diff --git a/src/commands/org/logout.ts b/src/commands/org/logout.ts index bc1f6ddd..2030eb74 100644 --- a/src/commands/org/logout.ts +++ b/src/commands/org/logout.ts @@ -41,6 +41,11 @@ export default class Logout extends SfCommand { aliases: ['targetusername', 'u'], deprecateAliases: true, }), + 'client-app': Flags.string({ + char: 'c', + summary: messages.getMessage('flags.client-app.summary'), + dependsOn: ['target-org'], + }), all: Flags.boolean({ char: 'a', summary: messages.getMessage('flags.all.summary'), @@ -59,6 +64,7 @@ export default class Logout extends SfCommand { loglevel, }; + // eslint-disable-next-line complexity public async run(): Promise { const { flags } = await this.parse(Logout); const targetUsername = @@ -70,6 +76,10 @@ export default class Logout extends SfCommand { throw messages.createError('noOrgSpecifiedWithNoPrompt'); } + if (flags['client-app']) { + return this.logoutClientApp(flags['client-app'], targetUsername); + } + if (this.jsonEnabled() && !targetUsername && !flags.all) { throw messages.createError('noOrgSpecifiedWithJson'); } @@ -114,6 +124,38 @@ export default class Logout extends SfCommand { } } + private async logoutClientApp(clientApp: string, username: string): Promise { + const authInfo = await AuthInfo.create({ + username, + }); + + const authFields = authInfo.getFields(true); + if (!authFields.clientApps) { + throw messages.createError('error.noLinkedApps', [username]); + } + + if (authFields.clientApps && !(clientApp in authFields.clientApps)) { + throw messages.createError('error.invalidClientApp', [username, clientApp]); + } + + // if logging out of the last client app, remove the whole `clientApps` object from the auth fields + if (Object.keys(authFields.clientApps).length === 1) { + await authInfo.save({ + clientApps: undefined, + }); + } else { + // just remove the specific client app entry + delete authFields.clientApps[clientApp]; + + await authInfo.save({ + clientApps: authFields.clientApps, + }); + } + + this.logSuccess(messages.getMessage('logoutClientAppSuccess', [clientApp, username])); + return [clientApp]; + } + /** Warning about logging out of a scratch org and losing access to it */ private maybeWarnScratchOrgs(orgs: OrgAuthorization[]): OrgAuthorization[] { if (orgs.some((org) => org.isScratchOrg)) { From 548147b9ec05149b1d843b785acaf2b9e72ebfa2 Mon Sep 17 00:00:00 2001 From: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> Date: Thu, 12 Jun 2025 06:24:53 -0700 Subject: [PATCH 8/9] fix: messages (#1322) --- messages/logout.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/messages/logout.md b/messages/logout.md index 5065d022..21cb1ec0 100644 --- a/messages/logout.md +++ b/messages/logout.md @@ -10,6 +10,8 @@ The process is similar if you specify --all, except that in the initial list of Be careful! If you log out of a scratch org without having access to its password, you can't access the scratch org again, either through the CLI or the Salesforce UI. +Use the --client-app flag to log out of the link you previously created between an authenticated user and a connected app or external client app; you create these links with "org login web --client-app". Run "org display" to get the list of client app names. + # examples - Interactively select the orgs to log out of: @@ -42,7 +44,7 @@ All orgs includes Dev Hubs, sandboxes, DE orgs, and expired, deleted, and unknow # flags.client-app.summary -Client app to log out of. +Client app to log out of. # logoutOrgCommandSuccess @@ -54,11 +56,11 @@ Successfully logged out of "%s" client app for user %s. # error.noLinkedApps -%s does not have any client app linked. +%s doesn't have any linked client apps. # error.invalidClientApp -%s does not have a client app named "%s" linked. +%s doesn't have a linked client app named "%s". # noOrgsFound From ffd778f794d3ae65b3d21e05ca55d525f971e03e Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Thu, 12 Jun 2025 13:12:33 -0300 Subject: [PATCH 9/9] chore: bump core --- package.json | 2 +- yarn.lock | 32 ++++---------------------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 6474e408..44493e61 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@inquirer/checkbox": "^2.5.0", "@inquirer/select": "^2.5.0", "@oclif/core": "^4", - "@salesforce/core": "^8.12.1-dev.0", + "@salesforce/core": "^8.13.0", "@salesforce/kit": "^3.2.3", "@salesforce/plugin-info": "^3.4.65", "@salesforce/sf-plugins-core": "^12.2.2", diff --git a/yarn.lock b/yarn.lock index 7809ad49..85e2504c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1688,34 +1688,10 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.1" -"@salesforce/core@^8.10.0", "@salesforce/core@^8.11.0", "@salesforce/core@^8.11.1", "@salesforce/core@^8.11.2", "@salesforce/core@^8.5.1", "@salesforce/core@^8.8.0": - version "8.12.0" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.12.0.tgz#a458cc3e39f4e7df57d94f0deaaa0fd0660b18c9" - integrity sha512-LJIjoQ3UQJ1r/xxdQcaG5bU8MfxeO/LJhrfK/7LZeHVtp1iOIgedbwPuVNzTzYciDWh8elborarrPM4uWjtu5g== - dependencies: - "@jsforce/jsforce-node" "^3.8.2" - "@salesforce/kit" "^3.2.2" - "@salesforce/schemas" "^1.9.0" - "@salesforce/ts-types" "^2.0.10" - ajv "^8.17.1" - change-case "^4.1.2" - fast-levenshtein "^3.0.0" - faye "^1.4.0" - form-data "^4.0.0" - js2xmlparser "^4.0.1" - jsonwebtoken "9.0.2" - jszip "3.10.1" - pino "^9.7.0" - pino-abstract-transport "^1.2.0" - pino-pretty "^11.3.0" - proper-lockfile "^4.1.2" - semver "^7.6.3" - ts-retry-promise "^0.8.1" - -"@salesforce/core@^8.12.1-dev.0": - version "8.12.1-dev.0" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.12.1-dev.0.tgz#bb5ba0cb3f189496ce2f67ec88291c96a0255ccb" - integrity sha512-2tF0Qn33F31WD3jnW9Ve6tXoSY2+CxrBscaixMkoHDRvKsEB6aLxJ5fJImTx1/Awen3bJY9td0xjTIehLfdmGw== +"@salesforce/core@^8.10.0", "@salesforce/core@^8.11.0", "@salesforce/core@^8.11.1", "@salesforce/core@^8.11.2", "@salesforce/core@^8.13.0", "@salesforce/core@^8.5.1", "@salesforce/core@^8.8.0": + version "8.13.0" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.13.0.tgz#ffc00a776c60a401d4385abfeb6cd7fee0d90f3b" + integrity sha512-FyAn0UGa93D0N++8poeJt7yEaWQH++qxrv/Wf4TjNaUCLoh19g57lrXuos3qDJPr8Ut4x6QjVxEc49XLy+vBkw== dependencies: "@jsforce/jsforce-node" "^3.8.2" "@salesforce/kit" "^3.2.2"