From 807853edf36de2b519082749c0c5c71e9a471930 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Tue, 12 May 2026 13:35:34 -0500 Subject: [PATCH 01/14] feat: add show-access-token command --- messages/org.auth.show-access-token.md | 41 +++++ package.json | 3 + src/commands/org/auth/show-access-token.ts | 83 +++++++++ test/nut/org/auth/show-access-token.nut.ts | 119 +++++++++++++ test/unit/org/auth/show-access-token.test.ts | 175 +++++++++++++++++++ 5 files changed, 421 insertions(+) create mode 100644 messages/org.auth.show-access-token.md create mode 100644 src/commands/org/auth/show-access-token.ts create mode 100644 test/nut/org/auth/show-access-token.nut.ts create mode 100644 test/unit/org/auth/show-access-token.test.ts diff --git a/messages/org.auth.show-access-token.md b/messages/org.auth.show-access-token.md new file mode 100644 index 00000000..1bafef59 --- /dev/null +++ b/messages/org.auth.show-access-token.md @@ -0,0 +1,41 @@ +# summary + +Show the access token for an org. + +# description + +Displays the current access token for the specified org. Because access tokens are sensitive credentials that grant full access to an org, this command prompts for confirmation before revealing the token unless you pass --no-prompt or --json. + +# flags.no-prompt.summary + +Skip the security warning and reveal the access token without confirmation. + +# prompt.show-access-token + +You are about to reveal the access token for "%s". This token grants full access to the org with your current permissions. Sharing or logging this token is equivalent to sharing your credentials. Do you want to continue? + +# warning.show-access-token + +This command exposes a sensitive Access Token that allows for subsequent activity using your current authenticated session. Sharing this information is equivalent to logging someone in under the current credential, resulting in unintended access and escalation of privilege. For additional information, please review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm + +# error.noAccessToken + +No access token found for "%s". The org may need to be re-authenticated. + +# examples + +- Show the access token for the default org: + + <%= config.bin %> <%= command.id %> + +- Show the access token for a specific org: + + <%= config.bin %> <%= command.id %> --target-org my-scratch-org + +- Show the access token without the confirmation prompt: + + <%= config.bin %> <%= command.id %> --target-org my-scratch-org --no-prompt + +- Get the access token as JSON for use in scripts: + + <%= config.bin %> <%= command.id %> --target-org my-scratch-org --json diff --git a/package.json b/package.json index d991e9a4..19719419 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,9 @@ }, "disable": { "description": "Disable source tracking in an org." + }, + "auth": { + "description": "Commands for printing sensitive auth info." } }, "external": true diff --git a/src/commands/org/auth/show-access-token.ts b/src/commands/org/auth/show-access-token.ts new file mode 100644 index 00000000..0c3fb04e --- /dev/null +++ b/src/commands/org/auth/show-access-token.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import { AuthInfo, Messages, Org, SfError } from '@salesforce/core'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-org', 'org.auth.show-access-token'); + +export type OrgAuthShowAccessTokenResult = { + accessToken: string; +}; + +export default class OrgAuthShowAccessToken extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + + public static readonly flags = { + 'target-org': Flags.requiredOrg(), + 'api-version': Flags.orgApiVersion(), + 'no-prompt': Flags.boolean({ + summary: messages.getMessage('flags.no-prompt.summary'), + char: 'p', + }), + }; + + private org!: Org; + + public async run(): Promise { + const { flags } = await this.parse(OrgAuthShowAccessToken); + + this.org = flags['target-org']; + this.org.getConnection(flags['api-version']); + try { + // The auth file can have a stale access token. Refresh it before getting the fields + await this.org.refreshAuth(); + } catch (error) { + // Even if this fails, we want to display the information we can read from the auth file + this.warn('Unable to refresh auth for org. Access token may be stale.'); + } + + // Don't ask for confirmation if --json or --no-prompt is passed + if (!this.jsonEnabled() && !flags['no-prompt']) { + const confirmed = await this.confirm({ + message: messages.getMessage('prompt.show-access-token', [this.org.getUsername()]), + }); + if (!confirmed) { + throw new SfError('Show access token confirmation denied.'); + } + } else { + // Note: We don't show this warning if the user has already been prompted + this.warn(messages.getMessage('warning.show-access-token')); + } + + const authInfo = await AuthInfo.create({ username: this.org.getUsername() }); + const { accessToken } = authInfo.getFields(true); + + if (!accessToken) { + throw messages.createError('error.noAccessToken', [this.org.getUsername()]); + } + + this.table({ + overflow: 'wrap', + data: [{ key: 'Access Token', value: accessToken }], + }); + + return { accessToken }; + } +} diff --git a/test/nut/org/auth/show-access-token.nut.ts b/test/nut/org/auth/show-access-token.nut.ts new file mode 100644 index 00000000..60ed7117 --- /dev/null +++ b/test/nut/org/auth/show-access-token.nut.ts @@ -0,0 +1,119 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { join } from 'node:path'; +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { assert, expect } from 'chai'; +import { accessTokenRegex } from '@salesforce/core'; +import { OrgAuthShowAccessTokenResult } from '../../../../src/commands/org/auth/show-access-token.js'; + +describe('org auth show-access-token NUTs', () => { + let session: TestSession; + let scratchUsername: string; + + before(async () => { + session = await TestSession.create({ + project: { name: 'showAccessToken' }, + devhubAuthStrategy: 'AUTO', + scratchOrgs: [ + { + config: join('config', 'project-scratch-def.json'), + setDefault: true, + }, + ], + }); + + const defaultOrg = session.orgs.get('default'); + assert(defaultOrg?.username); + scratchUsername = defaultOrg.username; + }); + + after(async () => { + await session?.clean(); + }); + + describe('--json --no-prompt', () => { + it('returns an access token matching the expected pattern', () => { + const result = execCmd( + `org auth show-access-token --target-org ${scratchUsername} --no-prompt --json`, + { ensureExitCode: 0 } + ).jsonOutput?.result; + assert(result); + expect(result).to.have.property('accessToken'); + // NOTE: We assert truthiness instead of values so that a failure diff does not expose an access token. + expect(accessTokenRegex.test(result.accessToken), 'accessToken should match the expected format').to.be.true; + }); + + it('includes the security warning in json output', () => { + const output = execCmd( + `org auth show-access-token --target-org ${scratchUsername} --no-prompt --json`, + { ensureExitCode: 0 } + ).jsonOutput; + assert(output); + expect(output.warnings).to.be.an('array'); + expect(output.warnings?.some((w) => w.includes('Access Token'))).to.be.true; + }); + }); + + describe('--json (without --no-prompt)', () => { + it('returns an access token without prompting', () => { + const result = execCmd( + `org auth show-access-token --target-org ${scratchUsername} --json`, + { ensureExitCode: 0 } + ).jsonOutput?.result; + assert(result); + expect(result).to.have.property('accessToken'); + expect(accessTokenRegex.test(result.accessToken), 'accessToken should match the expected format').to.be.true; + }); + }); + + describe('--no-prompt (without --json)', () => { + it('outputs a table containing the access token', () => { + const output = execCmd(`org auth show-access-token --target-org ${scratchUsername} --no-prompt`, { + ensureExitCode: 0, + }).shellOutput.stdout; + expect(output).to.include('Access Token'); + expect(accessTokenRegex.test(output), 'table output should contain a valid access token').to.be.true; + }); + + it('includes the security warning in stderr', () => { + const stderr = execCmd(`org auth show-access-token --target-org ${scratchUsername} --no-prompt`, { + ensureExitCode: 0, + }).shellOutput.stderr; + expect(stderr).to.include('Access Token'); + }); + }); + + describe('default org resolution', () => { + it('uses the default target-org when no --target-org is specified', () => { + const result = execCmd('org auth show-access-token --no-prompt --json', { + ensureExitCode: 0, + }).jsonOutput?.result; + assert(result); + expect(result).to.have.property('accessToken'); + expect(accessTokenRegex.test(result.accessToken), 'accessToken should match the expected format').to.be.true; + }); + }); + + describe('errors', () => { + it('fails when target org does not exist', () => { + const output = execCmd('org auth show-access-token --target-org nonexistent@user.org --no-prompt --json', { + ensureExitCode: 1, + }).jsonOutput; + assert(output); + expect(output.status).to.equal(1); + }); + }); +}); diff --git a/test/unit/org/auth/show-access-token.test.ts b/test/unit/org/auth/show-access-token.test.ts new file mode 100644 index 00000000..cfcbae53 --- /dev/null +++ b/test/unit/org/auth/show-access-token.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect } from 'chai'; +import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; +import { Messages, Org, SfError } from '@salesforce/core'; +import { stubSfCommandUx, stubPrompter } from '@salesforce/sf-plugins-core'; +import OrgAuthShowAccessToken from '../../../../src/commands/org/auth/show-access-token.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-org', 'org.auth.show-access-token'); + +describe('org auth show-access-token', () => { + const $$ = new TestContext(); + let testOrg: MockTestOrgData; + let sfCommandUxStubs: ReturnType; + let prompterStubs: ReturnType; + + beforeEach(() => { + testOrg = new MockTestOrgData(); + testOrg.orgId = '00Dxx0000000000'; + sfCommandUxStubs = stubSfCommandUx($$.SANDBOX); + prompterStubs = stubPrompter($$.SANDBOX); + }); + + afterEach(() => { + $$.restore(); + }); + + describe('interactive (no --json, no --no-prompt)', () => { + it('prompts with the correct message including the username', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username]); + expect(prompterStubs.confirm.callCount).to.equal(1); + expect(prompterStubs.confirm.firstCall.args[0]).to.deep.equal({ + message: messages.getMessage('prompt.show-access-token', [testOrg.username]), + }); + }); + + it('returns the access token when user confirms', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + const result = await OrgAuthShowAccessToken.run(['--target-org', testOrg.username]); + expect(result).to.have.property('accessToken', testOrg.accessToken); + }); + + it('displays the access token in a table when user confirms', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username]); + const data = sfCommandUxStubs.table.firstCall.args[0].data; + expect(data).to.deep.include({ key: 'Access Token', value: testOrg.accessToken }); + }); + + it('throws when user denies the prompt', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(false); + try { + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username]); + expect.fail('Expected command to throw'); + } catch (e) { + const err = e as SfError; + expect(err.message).to.equal('Show access token confirmation denied.'); + } + }); + + it('does not emit the security warning when prompting', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username]); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.not.include(messages.getMessage('warning.show-access-token')); + }); + }); + + describe('--no-prompt', () => { + it('skips the confirm prompt', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username, '--no-prompt']); + expect(prompterStubs.confirm.callCount).to.equal(0); + }); + + it('emits the security warning', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username, '--no-prompt']); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include(messages.getMessage('warning.show-access-token')); + }); + + it('returns the access token', async () => { + await $$.stubAuths(testOrg); + const result = await OrgAuthShowAccessToken.run(['--target-org', testOrg.username, '--no-prompt']); + expect(result).to.have.property('accessToken', testOrg.accessToken); + }); + + it('displays the access token in a table', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username, '--no-prompt']); + const data = sfCommandUxStubs.table.firstCall.args[0].data; + expect(data).to.deep.include({ key: 'Access Token', value: testOrg.accessToken }); + }); + }); + + describe('--json', () => { + it('skips the confirm prompt', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username, '--json']); + expect(prompterStubs.confirm.callCount).to.equal(0); + }); + + it('emits the security warning', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username, '--json']); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include(messages.getMessage('warning.show-access-token')); + }); + + it('returns the access token', async () => { + await $$.stubAuths(testOrg); + const result = await OrgAuthShowAccessToken.run(['--target-org', testOrg.username, '--json']); + expect(result).to.have.property('accessToken', testOrg.accessToken); + }); + }); + + describe('--json --no-prompt', () => { + it('skips the confirm prompt and emits the security warning', async () => { + await $$.stubAuths(testOrg); + const result = await OrgAuthShowAccessToken.run(['--target-org', testOrg.username, '--json', '--no-prompt']); + expect(prompterStubs.confirm.callCount).to.equal(0); + expect(result).to.have.property('accessToken', testOrg.accessToken); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include(messages.getMessage('warning.show-access-token')); + }); + }); + + describe('error: no access token', () => { + it('throws the noAccessToken error with the username', async () => { + // @ts-expect-error testing the case where accessToken is missing + testOrg.accessToken = undefined; + await $$.stubAuths(testOrg); + try { + await OrgAuthShowAccessToken.run(['--target-org', testOrg.username, '--no-prompt']); + expect.fail('Expected command to throw'); + } catch (e) { + const err = e as SfError; + expect(err.message).to.equal(messages.getMessage('error.noAccessToken', [testOrg.username])); + } + }); + }); + + describe('auth refresh failure', () => { + it('warns but continues when auth refresh fails', async () => { + await $$.stubAuths(testOrg); + $$.SANDBOX.stub(Org.prototype, 'refreshAuth').rejects(new Error('refresh failed')); + prompterStubs.confirm.resolves(true); + const result = await OrgAuthShowAccessToken.run(['--target-org', testOrg.username]); + expect(result).to.have.property('accessToken', testOrg.accessToken); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include('Unable to refresh auth for org. Access token may be stale.'); + }); + }); +}); From e2c1924bf179631a909c1e238d21a16a942e5a46 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Tue, 12 May 2026 13:38:04 -0500 Subject: [PATCH 02/14] chore: add schema --- schemas/org-auth-show__access__token.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 schemas/org-auth-show__access__token.json diff --git a/schemas/org-auth-show__access__token.json b/schemas/org-auth-show__access__token.json new file mode 100644 index 00000000..e66c9e2a --- /dev/null +++ b/schemas/org-auth-show__access__token.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/OrgAuthShowAccessTokenResult", + "definitions": { + "OrgAuthShowAccessTokenResult": { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + } + }, + "required": ["accessToken"], + "additionalProperties": false + } + } +} From 3b7905c4e16fd5a2a0bdb3b48a395b1dd5886a0e Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Tue, 12 May 2026 13:39:30 -0500 Subject: [PATCH 03/14] chore: add snapshot --- command-snapshot.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/command-snapshot.json b/command-snapshot.json index ea7d9577..b81d894f 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,4 +1,12 @@ [ + { + "alias": [], + "command": "org:auth:show-access-token", + "flagAliases": [], + "flagChars": ["o", "p"], + "flags": ["api-version", "flags-dir", "json", "no-prompt", "target-org"], + "plugin": "@salesforce/plugin-org" + }, { "alias": [], "command": "org:create:agent-user", From 760a797f414decca116616ff6a46b7e41842dda7 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Tue, 12 May 2026 14:32:21 -0500 Subject: [PATCH 04/14] chore: remove unnecessary api-version flag --- command-snapshot.json | 2 +- src/commands/org/auth/show-access-token.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/command-snapshot.json b/command-snapshot.json index b81d894f..e071f62f 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -4,7 +4,7 @@ "command": "org:auth:show-access-token", "flagAliases": [], "flagChars": ["o", "p"], - "flags": ["api-version", "flags-dir", "json", "no-prompt", "target-org"], + "flags": ["flags-dir", "json", "no-prompt", "target-org"], "plugin": "@salesforce/plugin-org" }, { diff --git a/src/commands/org/auth/show-access-token.ts b/src/commands/org/auth/show-access-token.ts index 0c3fb04e..3502d0f6 100644 --- a/src/commands/org/auth/show-access-token.ts +++ b/src/commands/org/auth/show-access-token.ts @@ -31,7 +31,6 @@ export default class OrgAuthShowAccessToken extends SfCommand Date: Tue, 12 May 2026 16:16:28 -0500 Subject: [PATCH 05/14] chore: minor refactor --- src/commands/org/auth/show-access-token.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/commands/org/auth/show-access-token.ts b/src/commands/org/auth/show-access-token.ts index 3502d0f6..88e4bfc2 100644 --- a/src/commands/org/auth/show-access-token.ts +++ b/src/commands/org/auth/show-access-token.ts @@ -15,7 +15,7 @@ */ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; -import { AuthInfo, Messages, Org, SfError } from '@salesforce/core'; +import { AuthInfo, Messages, SfError } from '@salesforce/core'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'org.auth.show-access-token'); @@ -37,15 +37,14 @@ export default class OrgAuthShowAccessToken extends SfCommand { const { flags } = await this.parse(OrgAuthShowAccessToken); - this.org = flags['target-org']; + const org = flags['target-org']; + const username = org.getUsername(); try { // The auth file can have a stale access token. Refresh it before getting the fields - await this.org.refreshAuth(); + await org.refreshAuth(); } catch (error) { // Even if this fails, we want to display the information we can read from the auth file this.warn('Unable to refresh auth for org. Access token may be stale.'); @@ -54,7 +53,7 @@ export default class OrgAuthShowAccessToken extends SfCommand Date: Tue, 12 May 2026 16:37:11 -0500 Subject: [PATCH 06/14] chore: longer timeout, timeout message --- src/commands/org/auth/show-access-token.ts | 3 ++- test/unit/org/auth/show-access-token.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/org/auth/show-access-token.ts b/src/commands/org/auth/show-access-token.ts index 88e4bfc2..8137d41c 100644 --- a/src/commands/org/auth/show-access-token.ts +++ b/src/commands/org/auth/show-access-token.ts @@ -54,9 +54,10 @@ export default class OrgAuthShowAccessToken extends SfCommand { expect(prompterStubs.confirm.callCount).to.equal(1); expect(prompterStubs.confirm.firstCall.args[0]).to.deep.equal({ message: messages.getMessage('prompt.show-access-token', [testOrg.username]), + ms: 30_000, }); }); @@ -73,7 +74,7 @@ describe('org auth show-access-token', () => { expect.fail('Expected command to throw'); } catch (e) { const err = e as SfError; - expect(err.message).to.equal('Show access token confirmation denied.'); + expect(err.message).to.equal('Show access token confirmation denied or timed out.'); } }); From 9b2c9330ab105374dfe4db8ea68338a2fcb67876 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Tue, 12 May 2026 16:44:39 -0500 Subject: [PATCH 07/14] feat: add show-sfdx-auth-url command --- command-snapshot.json | 8 + messages/org.auth.show-sfdx-auth-url.md | 41 +++++ schemas/org-auth-show__sfdx__auth__url.json | 16 ++ src/commands/org/auth/show-sfdx-auth-url.ts | 74 ++++++++ test/nut/org/auth/show-sfdx-auth-url.nut.ts | 124 +++++++++++++ test/unit/org/auth/show-sfdx-auth-url.test.ts | 172 ++++++++++++++++++ 6 files changed, 435 insertions(+) create mode 100644 messages/org.auth.show-sfdx-auth-url.md create mode 100644 schemas/org-auth-show__sfdx__auth__url.json create mode 100644 src/commands/org/auth/show-sfdx-auth-url.ts create mode 100644 test/nut/org/auth/show-sfdx-auth-url.nut.ts create mode 100644 test/unit/org/auth/show-sfdx-auth-url.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index e071f62f..32fac485 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -7,6 +7,14 @@ "flags": ["flags-dir", "json", "no-prompt", "target-org"], "plugin": "@salesforce/plugin-org" }, + { + "alias": [], + "command": "org:auth:show-sfdx-auth-url", + "flagAliases": [], + "flagChars": ["o", "p"], + "flags": ["flags-dir", "json", "no-prompt", "target-org"], + "plugin": "@salesforce/plugin-org" + }, { "alias": [], "command": "org:create:agent-user", diff --git a/messages/org.auth.show-sfdx-auth-url.md b/messages/org.auth.show-sfdx-auth-url.md new file mode 100644 index 00000000..ecff68b7 --- /dev/null +++ b/messages/org.auth.show-sfdx-auth-url.md @@ -0,0 +1,41 @@ +# summary + +Show the SFDX Auth URL for an org. + +# description + +Displays the SFDX Auth URL for the specified org. The SFDX Auth URL contains a refresh token that provides persistent access to the org without requiring re-authentication. This URL is only available for orgs authenticated via a web-based OAuth flow. Because this URL is equivalent to a permanent login credential, this command prompts for confirmation before revealing it unless you pass --no-prompt or --json. + +# flags.no-prompt.summary + +Skip the security warning and reveal the SFDX Auth URL without confirmation. + +# prompt.show-sfdx-auth-url + +You are about to reveal the SFDX Auth URL for "%s". This URL contains a refresh token that grants persistent access to the org without re-authentication. Anyone with this URL can authenticate to the org with your permissions. Do you want to continue? + +# warning.show-sfdx-auth-url + +This command exposes a sensitive SFDX Auth URL containing a refresh token that grants persistent access to the org. Unlike an access token, this credential does not expire and allows re-authentication without user interaction. Sharing this URL is equivalent to giving permanent login access to the org. For additional information, please review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_url.htm + +# error.noRefreshToken + +An SFDX Auth URL is not available for "%s". This URL is only available for orgs authenticated via a web-based login flow. Re-authenticate to the org using "sf org login web" to make it available. + +# examples + +- Show the SFDX Auth URL for the default org: + + <%= config.bin %> <%= command.id %> + +- Show the SFDX Auth URL for a specific org: + + <%= config.bin %> <%= command.id %> --target-org my-scratch-org + +- Show the SFDX Auth URL without the confirmation prompt: + + <%= config.bin %> <%= command.id %> --target-org my-scratch-org --no-prompt + +- Get the SFDX Auth URL as JSON for use in scripts: + + <%= config.bin %> <%= command.id %> --target-org my-scratch-org --json diff --git a/schemas/org-auth-show__sfdx__auth__url.json b/schemas/org-auth-show__sfdx__auth__url.json new file mode 100644 index 00000000..a11cd77e --- /dev/null +++ b/schemas/org-auth-show__sfdx__auth__url.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/OrgAuthShowSfdxAuthUrlResult", + "definitions": { + "OrgAuthShowSfdxAuthUrlResult": { + "type": "object", + "properties": { + "sfdxAuthUrl": { + "type": "string" + } + }, + "required": ["sfdxAuthUrl"], + "additionalProperties": false + } + } +} diff --git a/src/commands/org/auth/show-sfdx-auth-url.ts b/src/commands/org/auth/show-sfdx-auth-url.ts new file mode 100644 index 00000000..6d7f23c0 --- /dev/null +++ b/src/commands/org/auth/show-sfdx-auth-url.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import { AuthInfo, Messages, SfError } from '@salesforce/core'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-org', 'org.auth.show-sfdx-auth-url'); + +export type OrgAuthShowSfdxAuthUrlResult = { + sfdxAuthUrl: string; +}; + +export default class OrgAuthShowSfdxAuthUrl extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + + public static readonly flags = { + 'target-org': Flags.requiredOrg(), + 'no-prompt': Flags.boolean({ + summary: messages.getMessage('flags.no-prompt.summary'), + char: 'p', + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(OrgAuthShowSfdxAuthUrl); + + const org = flags['target-org']; + const username = org.getUsername(); + + if (!this.jsonEnabled() && !flags['no-prompt']) { + const confirmed = await this.confirm({ + message: messages.getMessage('prompt.show-sfdx-auth-url', [username]), + ms: 30_000, + }); + if (!confirmed) { + throw new SfError('Show SFDX auth URL confirmation denied or timed out.'); + } + } else { + this.warn(messages.getMessage('warning.show-sfdx-auth-url')); + } + + const authInfo = await AuthInfo.create({ username }); + const { refreshToken } = authInfo.getFields(true); + + if (!refreshToken) { + throw messages.createError('error.noRefreshToken', [username]); + } + + const sfdxAuthUrl = authInfo.getSfdxAuthUrl(); + + this.table({ + overflow: 'wrap', + data: [{ key: 'SFDX Auth URL', value: sfdxAuthUrl }], + }); + + return { sfdxAuthUrl }; + } +} diff --git a/test/nut/org/auth/show-sfdx-auth-url.nut.ts b/test/nut/org/auth/show-sfdx-auth-url.nut.ts new file mode 100644 index 00000000..e8fed556 --- /dev/null +++ b/test/nut/org/auth/show-sfdx-auth-url.nut.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { join } from 'node:path'; +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { assert, expect } from 'chai'; +import { sfdxAuthUrlRegex } from '@salesforce/core'; +import { OrgAuthShowSfdxAuthUrlResult } from '../../../../src/commands/org/auth/show-sfdx-auth-url.js'; + +describe('org auth show-sfdx-auth-url NUTs', () => { + let session: TestSession; + let scratchUsername: string; + + before(async () => { + session = await TestSession.create({ + project: { name: 'showAuthUrl' }, + // ------------------------------ + // NOTE: This will fail locally + // unless you have set the + // TESTKIT_AUTH_URL env var + // ------------------------------ + devhubAuthStrategy: 'AUTH_URL', + scratchOrgs: [ + { + config: join('config', 'project-scratch-def.json'), + setDefault: true, + }, + ], + }); + + const defaultOrg = session.orgs.get('default'); + assert(defaultOrg?.username); + scratchUsername = defaultOrg.username; + }); + + after(async () => { + await session?.clean(); + }); + + describe('--json --no-prompt', () => { + it('returns an SFDX auth URL matching the expected pattern', () => { + const result = execCmd( + `org auth show-sfdx-auth-url --target-org ${scratchUsername} --no-prompt --json`, + { ensureExitCode: 0 } + ).jsonOutput?.result; + assert(result); + expect(result).to.have.property('sfdxAuthUrl'); + // NOTE: We assert truthiness instead of values so that a failure diff does not expose credentials. + expect(sfdxAuthUrlRegex.test(result.sfdxAuthUrl), 'sfdxAuthUrl should match the expected format').to.be.true; + }); + + it('includes the security warning in json output', () => { + const output = execCmd( + `org auth show-sfdx-auth-url --target-org ${scratchUsername} --no-prompt --json`, + { ensureExitCode: 0 } + ).jsonOutput; + assert(output); + expect(output.warnings).to.be.an('array'); + expect(output.warnings?.some((w) => w.includes('Auth URL'))).to.be.true; + }); + }); + + describe('--json (without --no-prompt)', () => { + it('returns an SFDX auth URL without prompting', () => { + const result = execCmd( + `org auth show-sfdx-auth-url --target-org ${scratchUsername} --json`, + { ensureExitCode: 0 } + ).jsonOutput?.result; + assert(result); + expect(result).to.have.property('sfdxAuthUrl'); + expect(sfdxAuthUrlRegex.test(result.sfdxAuthUrl), 'sfdxAuthUrl should match the expected format').to.be.true; + }); + }); + + describe('--no-prompt (without --json)', () => { + it('outputs a table containing the auth URL', () => { + const output = execCmd(`org auth show-sfdx-auth-url --target-org ${scratchUsername} --no-prompt`, { + ensureExitCode: 0, + }).shellOutput.stdout; + expect(output).to.include('SFDX Auth URL'); + expect(sfdxAuthUrlRegex.test(output), 'table output should contain a valid SFDX auth URL').to.be.true; + }); + + it('includes the security warning in stderr', () => { + const stderr = execCmd(`org auth show-sfdx-auth-url --target-org ${scratchUsername} --no-prompt`, { + ensureExitCode: 0, + }).shellOutput.stderr; + expect(stderr).to.include('Auth URL'); + }); + }); + + describe('default org resolution', () => { + it('uses the default target-org when no --target-org is specified', () => { + const result = execCmd('org auth show-sfdx-auth-url --no-prompt --json', { + ensureExitCode: 0, + }).jsonOutput?.result; + assert(result); + expect(result).to.have.property('sfdxAuthUrl'); + expect(sfdxAuthUrlRegex.test(result.sfdxAuthUrl), 'sfdxAuthUrl should match the expected format').to.be.true; + }); + }); + + describe('errors', () => { + it('fails when target org does not exist', () => { + const output = execCmd('org auth show-sfdx-auth-url --target-org nonexistent@user.org --no-prompt --json', { + ensureExitCode: 1, + }).jsonOutput; + assert(output); + expect(output.status).to.equal(1); + }); + }); +}); diff --git a/test/unit/org/auth/show-sfdx-auth-url.test.ts b/test/unit/org/auth/show-sfdx-auth-url.test.ts new file mode 100644 index 00000000..7e5779b2 --- /dev/null +++ b/test/unit/org/auth/show-sfdx-auth-url.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect } from 'chai'; +import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; +import { Messages, SfError } from '@salesforce/core'; +import { stubSfCommandUx, stubPrompter } from '@salesforce/sf-plugins-core'; +import OrgAuthShowSfdxAuthUrl from '../../../../src/commands/org/auth/show-sfdx-auth-url.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-org', 'org.auth.show-sfdx-auth-url'); + +const refreshToken = 'mock.refresh_token'; + +describe('org auth show-sfdx-auth-url', () => { + const $$ = new TestContext(); + let testOrg: MockTestOrgData; + let sfCommandUxStubs: ReturnType; + let prompterStubs: ReturnType; + + beforeEach(() => { + testOrg = new MockTestOrgData(); + testOrg.orgId = '00Dxx0000000000'; + testOrg.refreshToken = refreshToken; + sfCommandUxStubs = stubSfCommandUx($$.SANDBOX); + prompterStubs = stubPrompter($$.SANDBOX); + }); + + afterEach(() => { + $$.restore(); + }); + + describe('interactive (no --json, no --no-prompt)', () => { + it('prompts with the correct message including the username', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username]); + expect(prompterStubs.confirm.callCount).to.equal(1); + expect(prompterStubs.confirm.firstCall.args[0]).to.deep.equal({ + message: messages.getMessage('prompt.show-sfdx-auth-url', [testOrg.username]), + ms: 30_000, + }); + }); + + it('returns the sfdxAuthUrl when user confirms', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + const result = await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username]); + expect(result).to.have.property('sfdxAuthUrl'); + expect(result.sfdxAuthUrl).to.include(refreshToken); + }); + + it('displays the auth URL in a table when user confirms', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username]); + const data = sfCommandUxStubs.table.firstCall.args[0].data; + expect(data[0].key).to.equal('SFDX Auth URL'); + expect(data[0].value).to.include(refreshToken); + }); + + it('throws when user denies the prompt', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(false); + try { + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username]); + expect.fail('Expected command to throw'); + } catch (e) { + const err = e as SfError; + expect(err.message).to.equal('Show SFDX auth URL confirmation denied or timed out.'); + } + }); + + it('does not emit the security warning when prompting', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username]); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.not.include(messages.getMessage('warning.show-sfdx-auth-url')); + }); + }); + + describe('--no-prompt', () => { + it('skips the confirm prompt', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username, '--no-prompt']); + expect(prompterStubs.confirm.callCount).to.equal(0); + }); + + it('emits the security warning', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username, '--no-prompt']); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include(messages.getMessage('warning.show-sfdx-auth-url')); + }); + + it('returns the sfdxAuthUrl', async () => { + await $$.stubAuths(testOrg); + const result = await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username, '--no-prompt']); + expect(result).to.have.property('sfdxAuthUrl'); + expect(result.sfdxAuthUrl).to.include(refreshToken); + }); + + it('displays the auth URL in a table', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username, '--no-prompt']); + const data = sfCommandUxStubs.table.firstCall.args[0].data; + expect(data[0].key).to.equal('SFDX Auth URL'); + expect(data[0].value).to.include(refreshToken); + }); + }); + + describe('--json', () => { + it('skips the confirm prompt', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username, '--json']); + expect(prompterStubs.confirm.callCount).to.equal(0); + }); + + it('emits the security warning', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username, '--json']); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include(messages.getMessage('warning.show-sfdx-auth-url')); + }); + + it('returns the sfdxAuthUrl', async () => { + await $$.stubAuths(testOrg); + const result = await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username, '--json']); + expect(result).to.have.property('sfdxAuthUrl'); + expect(result.sfdxAuthUrl).to.include(refreshToken); + }); + }); + + describe('--json --no-prompt', () => { + it('skips the confirm prompt and emits the security warning', async () => { + await $$.stubAuths(testOrg); + const result = await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username, '--json', '--no-prompt']); + expect(prompterStubs.confirm.callCount).to.equal(0); + expect(result).to.have.property('sfdxAuthUrl'); + expect(result.sfdxAuthUrl).to.include(refreshToken); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include(messages.getMessage('warning.show-sfdx-auth-url')); + }); + }); + + describe('error: no refresh token', () => { + it('throws the noRefreshToken error with the username', async () => { + testOrg.refreshToken = undefined; + await $$.stubAuths(testOrg); + try { + await OrgAuthShowSfdxAuthUrl.run(['--target-org', testOrg.username, '--no-prompt']); + expect.fail('Expected command to throw'); + } catch (e) { + const err = e as SfError; + expect(err.message).to.equal(messages.getMessage('error.noRefreshToken', [testOrg.username])); + } + }); + }); +}); From 32fbbc98c6313559094f0fa5ec08f70befeeda2b Mon Sep 17 00:00:00 2001 From: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> Date: Wed, 13 May 2026 08:36:17 -0700 Subject: [PATCH 08/14] quick edit Updated prompts and examples to clarify access token retrieval for an org. --- messages/org.auth.show-access-token.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/messages/org.auth.show-access-token.md b/messages/org.auth.show-access-token.md index 1bafef59..011923f8 100644 --- a/messages/org.auth.show-access-token.md +++ b/messages/org.auth.show-access-token.md @@ -1,10 +1,10 @@ # summary -Show the access token for an org. +Show the current access token for an org. # description -Displays the current access token for the specified org. Because access tokens are sensitive credentials that grant full access to an org, this command prompts for confirmation before revealing the token unless you pass --no-prompt or --json. +Because access tokens are sensitive credentials that grant full access to an org, this command prompts for confirmation before revealing the token. Skip confirmation by specifying either the --no-prompt or --json flag. # flags.no-prompt.summary @@ -12,11 +12,11 @@ Skip the security warning and reveal the access token without confirmation. # prompt.show-access-token -You are about to reveal the access token for "%s". This token grants full access to the org with your current permissions. Sharing or logging this token is equivalent to sharing your credentials. Do you want to continue? +You're about to reveal the access token for "%s". This token grants full access to the org with your current permissions. Sharing or logging this token is equivalent to sharing your credentials. Do you want to continue? # warning.show-access-token -This command exposes a sensitive Access Token that allows for subsequent activity using your current authenticated session. Sharing this information is equivalent to logging someone in under the current credential, resulting in unintended access and escalation of privilege. For additional information, please review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm +This command exposes a sensitive Access Token that allows for subsequent activity using your current authenticated session. Sharing this information is equivalent to logging someone in under the current credential, resulting in unintended access and escalation of privilege. For additional information about org authorization, please review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm. # error.noAccessToken @@ -28,14 +28,14 @@ No access token found for "%s". The org may need to be re-authenticated. <%= config.bin %> <%= command.id %> -- Show the access token for a specific org: +- Show the access token for an org with alias "my-org": - <%= config.bin %> <%= command.id %> --target-org my-scratch-org + <%= config.bin %> <%= command.id %> --target-org my-org - Show the access token without the confirmation prompt: - <%= config.bin %> <%= command.id %> --target-org my-scratch-org --no-prompt + <%= config.bin %> <%= command.id %> --target-org my-org --no-prompt - Get the access token as JSON for use in scripts: - <%= config.bin %> <%= command.id %> --target-org my-scratch-org --json + <%= config.bin %> <%= command.id %> --target-org my-org --json From fa484a5dfc241b2d8e5d1ee97e9092e44f2c8f51 Mon Sep 17 00:00:00 2001 From: Juliet Shackell Date: Wed, 13 May 2026 08:45:54 -0700 Subject: [PATCH 09/14] fix: few more edits --- messages/org.auth.show-access-token.md | 2 +- messages/org.auth.show-sfdx-auth-url.md | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/messages/org.auth.show-access-token.md b/messages/org.auth.show-access-token.md index 011923f8..ce7b03c5 100644 --- a/messages/org.auth.show-access-token.md +++ b/messages/org.auth.show-access-token.md @@ -16,7 +16,7 @@ You're about to reveal the access token for "%s". This token grants full access # warning.show-access-token -This command exposes a sensitive Access Token that allows for subsequent activity using your current authenticated session. Sharing this information is equivalent to logging someone in under the current credential, resulting in unintended access and escalation of privilege. For additional information about org authorization, please review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm. +This command exposes a sensitive Access Token that allows for subsequent activity using your current authenticated session. Sharing this information is equivalent to logging someone in under the current credential, resulting in unintended access and escalation of privilege. For additional information about org authorization, review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm. # error.noAccessToken diff --git a/messages/org.auth.show-sfdx-auth-url.md b/messages/org.auth.show-sfdx-auth-url.md index ecff68b7..f3b74987 100644 --- a/messages/org.auth.show-sfdx-auth-url.md +++ b/messages/org.auth.show-sfdx-auth-url.md @@ -4,7 +4,7 @@ Show the SFDX Auth URL for an org. # description -Displays the SFDX Auth URL for the specified org. The SFDX Auth URL contains a refresh token that provides persistent access to the org without requiring re-authentication. This URL is only available for orgs authenticated via a web-based OAuth flow. Because this URL is equivalent to a permanent login credential, this command prompts for confirmation before revealing it unless you pass --no-prompt or --json. +The SFDX Auth URL contains a refresh token that provides persistent access to the org without requiring re-authentication. This URL is only available for orgs authenticated via a web-based OAuth flow. Because this URL is equivalent to a permanent login credential, this command prompts for confirmation before revealing it. Skip confirmation by specifying either the --no-prompt or --json flag. # flags.no-prompt.summary @@ -12,11 +12,11 @@ Skip the security warning and reveal the SFDX Auth URL without confirmation. # prompt.show-sfdx-auth-url -You are about to reveal the SFDX Auth URL for "%s". This URL contains a refresh token that grants persistent access to the org without re-authentication. Anyone with this URL can authenticate to the org with your permissions. Do you want to continue? +You're about to reveal the SFDX Auth URL for "%s". This URL contains a refresh token that grants persistent access to the org without re-authentication. Anyone with this URL can authenticate to the org with your permissions. Do you want to continue? # warning.show-sfdx-auth-url -This command exposes a sensitive SFDX Auth URL containing a refresh token that grants persistent access to the org. Unlike an access token, this credential does not expire and allows re-authentication without user interaction. Sharing this URL is equivalent to giving permanent login access to the org. For additional information, please review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_url.htm +This command exposes a sensitive SFDX Auth URL containing a refresh token that grants persistent access to the org. Unlike an access token, this credential does not expire and allows re-authentication without user interaction. Sharing this URL is equivalent to giving permanent login access to the org. For additional information about org authorization, review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_url.htm. # error.noRefreshToken @@ -28,14 +28,14 @@ An SFDX Auth URL is not available for "%s". This URL is only available for orgs <%= config.bin %> <%= command.id %> -- Show the SFDX Auth URL for a specific org: +- Show the SFDX Auth URL for an org with alias "my-org": - <%= config.bin %> <%= command.id %> --target-org my-scratch-org + <%= config.bin %> <%= command.id %> --target-org my-org - Show the SFDX Auth URL without the confirmation prompt: - <%= config.bin %> <%= command.id %> --target-org my-scratch-org --no-prompt + <%= config.bin %> <%= command.id %> --target-org my-org --no-prompt - Get the SFDX Auth URL as JSON for use in scripts: - <%= config.bin %> <%= command.id %> --target-org my-scratch-org --json + <%= config.bin %> <%= command.id %> --target-org my-org --json From f635aa61212d585c11f060b671f36127f4128a13 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Thu, 14 May 2026 17:01:09 -0500 Subject: [PATCH 10/14] feat: show-user-password command --- command-snapshot.json | 8 + messages/org.auth.show-sfdx-auth-url.md | 6 +- messages/org.auth.show-user-password.md | 41 +++++ schemas/org-auth-show__user__password.json | 16 ++ src/commands/org/auth/show-user-password.ts | 72 ++++++++ test/nut/org/auth/show-user-password.nut.ts | 118 +++++++++++++ test/unit/org/auth/show-user-password.test.ts | 166 ++++++++++++++++++ 7 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 messages/org.auth.show-user-password.md create mode 100644 schemas/org-auth-show__user__password.json create mode 100644 src/commands/org/auth/show-user-password.ts create mode 100644 test/nut/org/auth/show-user-password.nut.ts create mode 100644 test/unit/org/auth/show-user-password.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index 32fac485..b19908a0 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -15,6 +15,14 @@ "flags": ["flags-dir", "json", "no-prompt", "target-org"], "plugin": "@salesforce/plugin-org" }, + { + "alias": [], + "command": "org:auth:show-user-password", + "flagAliases": [], + "flagChars": ["o", "p"], + "flags": ["flags-dir", "json", "no-prompt", "target-org"], + "plugin": "@salesforce/plugin-org" + }, { "alias": [], "command": "org:create:agent-user", diff --git a/messages/org.auth.show-sfdx-auth-url.md b/messages/org.auth.show-sfdx-auth-url.md index f3b74987..f4e157f9 100644 --- a/messages/org.auth.show-sfdx-auth-url.md +++ b/messages/org.auth.show-sfdx-auth-url.md @@ -4,7 +4,7 @@ Show the SFDX Auth URL for an org. # description -The SFDX Auth URL contains a refresh token that provides persistent access to the org without requiring re-authentication. This URL is only available for orgs authenticated via a web-based OAuth flow. Because this URL is equivalent to a permanent login credential, this command prompts for confirmation before revealing it. Skip confirmation by specifying either the --no-prompt or --json flag. +Shows the SFDX Auth URL for an org. This URL is only available for orgs authenticated via a web-based OAuth flow. This command prompts for confirmation before revealing it. Skip confirmation by specifying either the --no-prompt or --json flag. # flags.no-prompt.summary @@ -12,11 +12,11 @@ Skip the security warning and reveal the SFDX Auth URL without confirmation. # prompt.show-sfdx-auth-url -You're about to reveal the SFDX Auth URL for "%s". This URL contains a refresh token that grants persistent access to the org without re-authentication. Anyone with this URL can authenticate to the org with your permissions. Do you want to continue? +You're about to reveal the SFDX Auth URL for "%s". This URL contains a refresh token that can be used to authenticate to the org without user interaction. Do you want to continue? # warning.show-sfdx-auth-url -This command exposes a sensitive SFDX Auth URL containing a refresh token that grants persistent access to the org. Unlike an access token, this credential does not expire and allows re-authentication without user interaction. Sharing this URL is equivalent to giving permanent login access to the org. For additional information about org authorization, review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_url.htm. +This command exposes an SFDX Auth URL. Unlike an access token, this credential contains a refresh token that allows extended access to an org. Avoid sharing or logging this URL. For additional information about org authorization, review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_url.htm. # error.noRefreshToken diff --git a/messages/org.auth.show-user-password.md b/messages/org.auth.show-user-password.md new file mode 100644 index 00000000..775171ca --- /dev/null +++ b/messages/org.auth.show-user-password.md @@ -0,0 +1,41 @@ +# summary + +Show the stored password for an org's user. + +# description + +Passwords are only available for orgs where a password was previously generated using "sf org generate password". Because passwords are sensitive credentials, this command prompts for confirmation before revealing it. Skip confirmation by specifying either the --no-prompt or --json flag. + +# flags.no-prompt.summary + +Skip the security warning and reveal the password without confirmation. + +# prompt.show-user-password + +You're about to reveal the password for "%s". Do you want to continue? + +# warning.show-user-password + +This command exposes a user password. While a password alone is not sufficient to gain access to an org (additional factors like a security token or an enabled OAuth username-password flow are required), treat it as a sensitive credential and avoid sharing or logging it. + +# error.noPassword + +No password found for "%s". A password is only available if one was previously generated using "sf org generate password". + +# examples + +- Show the password for the default org's user: + + <%= config.bin %> <%= command.id %> + +- Show the password for an org with alias "my-org": + + <%= config.bin %> <%= command.id %> --target-org my-org + +- Show the password without the confirmation prompt: + + <%= config.bin %> <%= command.id %> --target-org my-org --no-prompt + +- Get the password as JSON for use in scripts: + + <%= config.bin %> <%= command.id %> --target-org my-org --json diff --git a/schemas/org-auth-show__user__password.json b/schemas/org-auth-show__user__password.json new file mode 100644 index 00000000..948a229b --- /dev/null +++ b/schemas/org-auth-show__user__password.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/OrgAuthShowUserPasswordResult", + "definitions": { + "OrgAuthShowUserPasswordResult": { + "type": "object", + "properties": { + "password": { + "type": "string" + } + }, + "required": ["password"], + "additionalProperties": false + } + } +} diff --git a/src/commands/org/auth/show-user-password.ts b/src/commands/org/auth/show-user-password.ts new file mode 100644 index 00000000..2fe0749a --- /dev/null +++ b/src/commands/org/auth/show-user-password.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import { AuthInfo, Messages, SfError } from '@salesforce/core'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-org', 'org.auth.show-user-password'); + +export type OrgAuthShowUserPasswordResult = { + password: string; +}; + +export default class OrgAuthShowUserPassword extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + + public static readonly flags = { + 'target-org': Flags.requiredOrg(), + 'no-prompt': Flags.boolean({ + summary: messages.getMessage('flags.no-prompt.summary'), + char: 'p', + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(OrgAuthShowUserPassword); + + const org = flags['target-org']; + const username = org.getUsername(); + + if (!this.jsonEnabled() && !flags['no-prompt']) { + const confirmed = await this.confirm({ + message: messages.getMessage('prompt.show-user-password', [username]), + ms: 30_000, + }); + if (!confirmed) { + throw new SfError('Show user password confirmation denied or timed out.'); + } + } else { + this.warn(messages.getMessage('warning.show-user-password')); + } + + const authInfo = await AuthInfo.create({ username }); + const { password } = authInfo.getFields(true); + + if (!password) { + throw messages.createError('error.noPassword', [username]); + } + + this.table({ + overflow: 'wrap', + data: [{ key: 'Password', value: password }], + }); + + return { password }; + } +} diff --git a/test/nut/org/auth/show-user-password.nut.ts b/test/nut/org/auth/show-user-password.nut.ts new file mode 100644 index 00000000..b21c43b1 --- /dev/null +++ b/test/nut/org/auth/show-user-password.nut.ts @@ -0,0 +1,118 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { join } from 'node:path'; +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { assert, expect } from 'chai'; +import { OrgAuthShowUserPasswordResult } from '../../../../src/commands/org/auth/show-user-password.js'; + +describe('org auth show-user-password NUTs', () => { + let session: TestSession; + let scratchUsername: string; + + before(async () => { + session = await TestSession.create({ + project: { name: 'showUserPassword' }, + devhubAuthStrategy: 'AUTO', + scratchOrgs: [ + { + config: join('config', 'project-scratch-def.json'), + setDefault: true, + }, + ], + }); + + const defaultOrg = session.orgs.get('default'); + assert(defaultOrg?.username); + scratchUsername = defaultOrg.username; + + // Generate a password for the scratch org user + execCmd(`org generate password --target-org ${scratchUsername}`, { ensureExitCode: 0 }); + }); + + after(async () => { + await session?.clean(); + }); + + describe('--json --no-prompt', () => { + it('returns a password', () => { + const result = execCmd( + `org auth show-user-password --target-org ${scratchUsername} --no-prompt --json`, + { ensureExitCode: 0 } + ).jsonOutput?.result; + assert(result); + expect(result).to.have.property('password'); + // NOTE: We assert truthiness instead of values so that a failure diff does not expose credentials. + expect(result.password.length > 0, 'password should not be empty').to.be.true; + }); + + it('includes the security warning in json output', () => { + const output = execCmd( + `org auth show-user-password --target-org ${scratchUsername} --no-prompt --json`, + { ensureExitCode: 0 } + ).jsonOutput; + assert(output); + expect(output.warnings).to.be.an('array'); + expect(output.warnings?.some((w) => w.includes('password'))).to.be.true; + }); + }); + + describe('--json (without --no-prompt)', () => { + it('returns a password without prompting', () => { + const result = execCmd( + `org auth show-user-password --target-org ${scratchUsername} --json`, + { ensureExitCode: 0 } + ).jsonOutput?.result; + assert(result); + expect(result.password.length > 0, 'password should not be empty').to.be.true; + }); + }); + + describe('--no-prompt (without --json)', () => { + it('outputs a table containing the password', () => { + const output = execCmd(`org auth show-user-password --target-org ${scratchUsername} --no-prompt`, { + ensureExitCode: 0, + }).shellOutput.stdout; + expect(output).to.include('Password'); + }); + + it('includes the security warning in stderr', () => { + const stderr = execCmd(`org auth show-user-password --target-org ${scratchUsername} --no-prompt`, { + ensureExitCode: 0, + }).shellOutput.stderr; + expect(stderr).to.include('password'); + }); + }); + + describe('default org resolution', () => { + it('uses the default target-org when no --target-org is specified', () => { + const result = execCmd('org auth show-user-password --no-prompt --json', { + ensureExitCode: 0, + }).jsonOutput?.result; + assert(result); + expect(result.password.length > 0, 'password should not be empty').to.be.true; + }); + }); + + describe('errors', () => { + it('fails when target org does not exist', () => { + const output = execCmd('org auth show-user-password --target-org nonexistent@user.org --no-prompt --json', { + ensureExitCode: 1, + }).jsonOutput; + assert(output); + expect(output.status).to.equal(1); + }); + }); +}); diff --git a/test/unit/org/auth/show-user-password.test.ts b/test/unit/org/auth/show-user-password.test.ts new file mode 100644 index 00000000..67089792 --- /dev/null +++ b/test/unit/org/auth/show-user-password.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect } from 'chai'; +import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; +import { Messages, SfError } from '@salesforce/core'; +import { stubSfCommandUx, stubPrompter } from '@salesforce/sf-plugins-core'; +import OrgAuthShowUserPassword from '../../../../src/commands/org/auth/show-user-password.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-org', 'org.auth.show-user-password'); + +const mockPassword = 'mock.password.123'; + +describe('org auth show-user-password', () => { + const $$ = new TestContext(); + let testOrg: MockTestOrgData; + let sfCommandUxStubs: ReturnType; + let prompterStubs: ReturnType; + + beforeEach(() => { + testOrg = new MockTestOrgData(); + testOrg.orgId = '00Dxx0000000000'; + testOrg.password = mockPassword; + sfCommandUxStubs = stubSfCommandUx($$.SANDBOX); + prompterStubs = stubPrompter($$.SANDBOX); + }); + + afterEach(() => { + $$.restore(); + }); + + describe('interactive (no --json, no --no-prompt)', () => { + it('prompts with the correct message including the username', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username]); + expect(prompterStubs.confirm.callCount).to.equal(1); + expect(prompterStubs.confirm.firstCall.args[0]).to.deep.equal({ + message: messages.getMessage('prompt.show-user-password', [testOrg.username]), + ms: 30_000, + }); + }); + + it('returns the password when user confirms', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + const result = await OrgAuthShowUserPassword.run(['--target-org', testOrg.username]); + expect(result).to.have.property('password', mockPassword); + }); + + it('displays the password in a table when user confirms', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username]); + const data = sfCommandUxStubs.table.firstCall.args[0].data; + expect(data).to.deep.include({ key: 'Password', value: mockPassword }); + }); + + it('throws when user denies the prompt', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(false); + try { + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username]); + expect.fail('Expected command to throw'); + } catch (e) { + const err = e as SfError; + expect(err.message).to.equal('Show user password confirmation denied or timed out.'); + } + }); + + it('does not emit the security warning when prompting', async () => { + await $$.stubAuths(testOrg); + prompterStubs.confirm.resolves(true); + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username]); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.not.include(messages.getMessage('warning.show-user-password')); + }); + }); + + describe('--no-prompt', () => { + it('skips the confirm prompt', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username, '--no-prompt']); + expect(prompterStubs.confirm.callCount).to.equal(0); + }); + + it('emits the security warning', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username, '--no-prompt']); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include(messages.getMessage('warning.show-user-password')); + }); + + it('returns the password', async () => { + await $$.stubAuths(testOrg); + const result = await OrgAuthShowUserPassword.run(['--target-org', testOrg.username, '--no-prompt']); + expect(result).to.have.property('password', mockPassword); + }); + + it('displays the password in a table', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username, '--no-prompt']); + const data = sfCommandUxStubs.table.firstCall.args[0].data; + expect(data).to.deep.include({ key: 'Password', value: mockPassword }); + }); + }); + + describe('--json', () => { + it('skips the confirm prompt', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username, '--json']); + expect(prompterStubs.confirm.callCount).to.equal(0); + }); + + it('emits the security warning', async () => { + await $$.stubAuths(testOrg); + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username, '--json']); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include(messages.getMessage('warning.show-user-password')); + }); + + it('returns the password', async () => { + await $$.stubAuths(testOrg); + const result = await OrgAuthShowUserPassword.run(['--target-org', testOrg.username, '--json']); + expect(result).to.have.property('password', mockPassword); + }); + }); + + describe('--json --no-prompt', () => { + it('skips the confirm prompt and emits the security warning', async () => { + await $$.stubAuths(testOrg); + const result = await OrgAuthShowUserPassword.run(['--target-org', testOrg.username, '--json', '--no-prompt']); + expect(prompterStubs.confirm.callCount).to.equal(0); + expect(result).to.have.property('password', mockPassword); + const warnCalls = sfCommandUxStubs.warn.getCalls().flatMap((c) => c.args); + expect(warnCalls).to.include(messages.getMessage('warning.show-user-password')); + }); + }); + + describe('error: no password', () => { + it('throws the noPassword error with the username', async () => { + testOrg.password = undefined; + await $$.stubAuths(testOrg); + try { + await OrgAuthShowUserPassword.run(['--target-org', testOrg.username, '--no-prompt']); + expect.fail('Expected command to throw'); + } catch (e) { + const err = e as SfError; + expect(err.message).to.equal(messages.getMessage('error.noPassword', [testOrg.username])); + } + }); + }); +}); From a78d8eca123aeb66260a6bd5e850c9a20490d734 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Thu, 14 May 2026 17:11:12 -0500 Subject: [PATCH 11/14] chore: pw messaging --- messages/org.auth.show-user-password.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messages/org.auth.show-user-password.md b/messages/org.auth.show-user-password.md index 775171ca..17d15306 100644 --- a/messages/org.auth.show-user-password.md +++ b/messages/org.auth.show-user-password.md @@ -4,7 +4,7 @@ Show the stored password for an org's user. # description -Passwords are only available for orgs where a password was previously generated using "sf org generate password". Because passwords are sensitive credentials, this command prompts for confirmation before revealing it. Skip confirmation by specifying either the --no-prompt or --json flag. +Passwords are only available for orgs where a password was previously generated, such as by running "sf org generate password" or "sf org create user". Because passwords are sensitive credentials, this command prompts for confirmation before revealing it. Skip confirmation by specifying either the --no-prompt or --json flag. # flags.no-prompt.summary @@ -20,7 +20,7 @@ This command exposes a user password. While a password alone is not sufficient t # error.noPassword -No password found for "%s". A password is only available if one was previously generated using "sf org generate password". +No password found for "%s". A password is only available if one was previously generated, such as by running "sf org generate password" or "sf org create user". # examples From 07b00b97882d721cf07f82cc71eabc024e731de8 Mon Sep 17 00:00:00 2001 From: Willhoit Date: Fri, 15 May 2026 09:29:13 -0500 Subject: [PATCH 12/14] Apply suggestion from @iowillhoit --- messages/org.auth.show-user-password.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/messages/org.auth.show-user-password.md b/messages/org.auth.show-user-password.md index 17d15306..5552253e 100644 --- a/messages/org.auth.show-user-password.md +++ b/messages/org.auth.show-user-password.md @@ -4,7 +4,9 @@ Show the stored password for an org's user. # description -Passwords are only available for orgs where a password was previously generated, such as by running "sf org generate password" or "sf org create user". Because passwords are sensitive credentials, this command prompts for confirmation before revealing it. Skip confirmation by specifying either the --no-prompt or --json flag. +This command shows only passwords that were generated locally in your DX project with either the "org generate password" or "org create user" CLI command. If you generated a password for a user in Setup in your org, you can't show it with this command. + +Because passwords are sensitive credentials, this command prompts for confirmation before revealing it. Skip confirmation by specifying either the --no-prompt or --json flag. # flags.no-prompt.summary From f1869aea687ca3184b3d2f2963171dfb11989fcf Mon Sep 17 00:00:00 2001 From: Willhoit Date: Fri, 15 May 2026 11:24:00 -0500 Subject: [PATCH 13/14] Update messages/org.auth.show-user-password.md --- messages/org.auth.show-user-password.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/org.auth.show-user-password.md b/messages/org.auth.show-user-password.md index 5552253e..39297d49 100644 --- a/messages/org.auth.show-user-password.md +++ b/messages/org.auth.show-user-password.md @@ -22,7 +22,7 @@ This command exposes a user password. While a password alone is not sufficient t # error.noPassword -No password found for "%s". A password is only available if one was previously generated, such as by running "sf org generate password" or "sf org create user". +No password found for "%s". A password is available only if one was previously generated locally in your DX project by running either "sf org generate password" or "sf org create user". If you generated a password in your org using Setup, it's not available to this command. # examples From 3a6422d3c309540cdfc30b3708a1dda87638589b Mon Sep 17 00:00:00 2001 From: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> Date: Fri, 15 May 2026 09:33:06 -0700 Subject: [PATCH 14/14] fix: Apply suggestions from code review Co-authored-by: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> --- messages/org.auth.show-user-password.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/org.auth.show-user-password.md b/messages/org.auth.show-user-password.md index 39297d49..18c13bd2 100644 --- a/messages/org.auth.show-user-password.md +++ b/messages/org.auth.show-user-password.md @@ -18,7 +18,7 @@ You're about to reveal the password for "%s". Do you want to continue? # warning.show-user-password -This command exposes a user password. While a password alone is not sufficient to gain access to an org (additional factors like a security token or an enabled OAuth username-password flow are required), treat it as a sensitive credential and avoid sharing or logging it. +This command exposes a user password. While a password alone isn't sufficient to gain access to an org (additional factors like a security token or an enabled OAuth username-password flow are required), treat it as a sensitive credential and avoid sharing or logging it. # error.noPassword