diff --git a/command-snapshot.json b/command-snapshot.json index b56050f2..1d95bc20 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -31,30 +31,6 @@ ], "plugin": "@salesforce/plugin-auth" }, - { - "alias": ["force:auth:device:login", "auth:device:login"], - "command": "org:login:device", - "flagAliases": [ - "clientid", - "instanceurl", - "setalias", - "setdefaultdevhub", - "setdefaultdevhubusername", - "setdefaultusername" - ], - "flagChars": ["a", "d", "i", "r", "s"], - "flags": [ - "alias", - "client-id", - "flags-dir", - "instance-url", - "json", - "loglevel", - "set-default", - "set-default-dev-hub" - ], - "plugin": "@salesforce/plugin-auth" - }, { "alias": ["force:auth:jwt:grant", "auth:jwt:grant"], "command": "org:login:jwt", diff --git a/messages/device.login.md b/messages/device.login.md deleted file mode 100644 index a37b11b1..00000000 --- a/messages/device.login.md +++ /dev/null @@ -1,35 +0,0 @@ -# summary - -Authorize an org using a device code. - -# description - -Use this command to allow a device to connect to an org. - -When you run this command, it first displays an 8-digit device code and the URL for verifying the code on your org. The default instance URL is https://login.salesforce.com, so if the org you're authorizing is on a different instance, use the --instance-url. The command waits while you complete the verification. Open a browser and navigate to the displayed verification URL, enter the code, then click Connect. If you aren't already logged into your org, log in, and then you're prompted to allow the device to connect to the org. After you successfully authorize the org, you can close the browser window. - -# examples - -- Authorize an org using a device code, give the org the alias TestOrg1, and set it as your default Dev Hub org: - - <%= config.bin %> <%= command.id %> --set-default-dev-hub --alias TestOrg1 - -- Authorize an org in which you've created a custom connected app with the specified client ID (consumer key): - - <%= config.bin %> <%= command.id %> --client-id - -- Authorize a sandbox org with the specified instance URL: - - <%= config.bin %> <%= command.id %> --instance-url https://MyDomainName--SandboxName.sandbox.my.salesforce.com - -# actionRequired - -Action Required! - -# enterCode - -Enter %s device code in this verification URL: %s - -# success - -Login successful for %s. You can now close the browser. diff --git a/package.json b/package.json index e5261fcf..4f19d1a2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/plugin-auth", "description": "plugin for sf auth commands", - "version": "3.9.26", + "version": "4.0.0", "author": "Salesforce", "bugs": "https://github.com/forcedotcom/cli/issues", "dependencies": { @@ -75,9 +75,6 @@ }, "access-token": { "description": "authorize an org using an access token" - }, - "device": { - "description": "authorize an org using a device code" } } }, diff --git a/schemas/org-login-device.json b/schemas/org-login-device.json deleted file mode 100644 index 160350f6..00000000 --- a/schemas/org-login-device.json +++ /dev/null @@ -1,211 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/DeviceLoginResult", - "definitions": { - "DeviceLoginResult": { - "anyOf": [ - { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Optional%3CAnyJson%3E" - }, - "properties": { - "device_code": { - "type": "string" - }, - "interval": { - "type": "number" - }, - "user_code": { - "type": "string" - }, - "verification_uri": { - "type": "string" - }, - "clientApps": { - "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" - }, - "alias": { - "type": "string" - }, - "authCode": { - "type": "string" - }, - "clientId": { - "type": "string" - }, - "clientSecret": { - "type": "string" - }, - "created": { - "type": "string" - }, - "createdOrgInstance": { - "type": "string" - }, - "devHubUsername": { - "type": "string" - }, - "instanceUrl": { - "type": "string" - }, - "instanceApiVersion": { - "type": "string" - }, - "instanceApiVersionLastRetrieved": { - "type": "string" - }, - "isDevHub": { - "type": "boolean" - }, - "loginUrl": { - "type": "string" - }, - "orgId": { - "type": "string" - }, - "password": { - "type": "string" - }, - "privateKey": { - "type": "string" - }, - "refreshToken": { - "type": "string" - }, - "scratchAdminUsername": { - "type": "string" - }, - "snapshot": { - "type": "string" - }, - "userId": { - "type": "string" - }, - "username": { - "type": "string" - }, - "usernames": { - "type": "array", - "items": { - "type": "string" - } - }, - "userProfileName": { - "type": "string" - }, - "expirationDate": { - "type": "string" - }, - "tracksSource": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "instanceName": { - "type": "string" - }, - "namespacePrefix": { - "type": ["string", "null"] - }, - "isSandbox": { - "type": "boolean" - }, - "isScratch": { - "type": "boolean" - }, - "trailExpirationDate": { - "type": ["string", "null"] - } - }, - "required": ["device_code", "interval", "user_code", "verification_uri"] - }, - { - "type": "object", - "additionalProperties": { - "not": {} - } - } - ] - }, - "Optional": { - "anyOf": [ - { - "$ref": "#/definitions/AnyJson" - }, - { - "not": {} - } - ], - "description": "A union type for either the parameterized type `T` or `undefined` -- the opposite of {@link NonOptional } ." - }, - "AnyJson": { - "anyOf": [ - { - "$ref": "#/definitions/JsonPrimitive" - }, - { - "$ref": "#/definitions/JsonCollection" - } - ], - "description": "Any valid JSON value." - }, - "JsonPrimitive": { - "type": ["null", "boolean", "number", "string"], - "description": "Any valid JSON primitive value." - }, - "JsonCollection": { - "anyOf": [ - { - "$ref": "#/definitions/JsonMap" - }, - { - "$ref": "#/definitions/JsonArray" - } - ], - "description": "Any valid JSON collection value." - }, - "JsonMap": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Optional%3CAnyJson%3E" - }, - "properties": {}, - "description": "Any JSON-compatible object." - }, - "JsonArray": { - "type": "array", - "items": { - "$ref": "#/definitions/AnyJson" - }, - "description": "Any JSON-compatible array." - } - } -} diff --git a/src/commands/org/login/device.ts b/src/commands/org/login/device.ts deleted file mode 100644 index f899c98f..00000000 --- a/src/commands/org/login/device.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 { - type AuthFields, - AuthInfo, - DeviceOauthService, - Messages, - type OAuth2Config, - type DeviceCodeResponse, -} from '@salesforce/core'; -import { Flags, SfCommand, loglevel, Ux } from '@salesforce/sf-plugins-core'; -import common from '../../../common.js'; - -Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-auth', 'device.login'); -const commonMessages = Messages.loadMessages('@salesforce/plugin-auth', 'messages'); - -export type DeviceLoginResult = (AuthFields & DeviceCodeResponse) | Record; - -export default class LoginDevice 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 aliases = ['force:auth:device:login', 'auth:device:login']; - public static readonly deprecateAliases = true; - public static readonly hidden = true; - public static readonly deprecated = true; - - public static readonly flags = { - 'client-id': Flags.string({ - char: 'i', - summary: commonMessages.getMessage('flags.client-id.summary'), - deprecateAliases: true, - aliases: ['clientid'], - }), - 'instance-url': Flags.url({ - char: 'r', - summary: commonMessages.getMessage('flags.instance-url.summary'), - description: commonMessages.getMessage('flags.instance-url.description'), - deprecateAliases: true, - aliases: ['instanceurl'], - }), - 'set-default-dev-hub': Flags.boolean({ - char: 'd', - summary: commonMessages.getMessage('flags.set-default-dev-hub.summary'), - deprecateAliases: true, - aliases: ['setdefaultdevhub', 'setdefaultdevhubusername'], - }), - 'set-default': Flags.boolean({ - char: 's', - summary: commonMessages.getMessage('flags.set-default.summary'), - deprecateAliases: true, - aliases: ['setdefaultusername'], - }), - alias: Flags.string({ - char: 'a', - summary: commonMessages.getMessage('flags.alias.summary'), - deprecateAliases: true, - aliases: ['setalias'], - }), - loglevel, - }; - - public async run(): Promise { - const { flags } = await this.parse(LoginDevice); - if (await common.shouldExitCommand(false)) return {}; - - const oauthConfig: OAuth2Config = { - loginUrl: await common.resolveLoginUrl(flags['instance-url']?.href), - clientId: flags['client-id'], - }; - - const deviceOauthService = await DeviceOauthService.create(oauthConfig); - const loginData = await deviceOauthService.requestDeviceLogin(); - - if (this.jsonEnabled()) { - new Ux().log(JSON.stringify(loginData, null, 2)); - } else { - this.log(); - this.warn('Device Oauth flow is deprecated and will be removed mid January 2026\n'); - this.styledHeader(messages.getMessage('actionRequired')); - this.log(messages.getMessage('enterCode', [loginData.user_code, loginData.verification_uri])); - this.log(); - } - - const approval = await deviceOauthService.awaitDeviceApproval(loginData); - if (approval) { - const authInfo = await deviceOauthService.authorizeAndSave(approval); - await authInfo.handleAliasAndDefaultSettings({ - alias: flags.alias, - setDefault: flags['set-default'], - setDefaultDevHub: flags['set-default-dev-hub'], - }); - const fields = authInfo.getFields(true); - await AuthInfo.identifyPossibleScratchOrgs(fields, authInfo); - const successMsg = messages.getMessage('success', [fields.username]); - this.logSuccess(successMsg); - return { ...fields, ...loginData }; - } else { - return {}; - } - } -} diff --git a/test/commands/org/login/login.device.test.ts b/test/commands/org/login/login.device.test.ts deleted file mode 100644 index f379c3b1..00000000 --- a/test/commands/org/login/login.device.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable camelcase */ - -import { type AuthFields, AuthInfo, type DeviceCodeResponse, DeviceOauthService } from '@salesforce/core'; -import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; -import { StubbedType, stubInterface, stubMethod } from '@salesforce/ts-sinon'; -import { expect } from 'chai'; -import { SfCommand, stubUx } from '@salesforce/sf-plugins-core'; -import Login from '../../../../src/commands/org/login/device.js'; - -type Options = { - approvalTimesout?: boolean; - approvalFails?: boolean; -}; - -describe('org:login:device', () => { - const $$ = new TestContext(); - - const testData = new MockTestOrgData(); - const mockAction: DeviceCodeResponse = { - device_code: '1234', - interval: 5000, - user_code: '1234', - verification_uri: 'https://login.salesforce.com', - }; - - let authFields: AuthFields; - let authInfoStub: StubbedType; - - async function prepareStubs(options: Options = {}): Promise { - authFields = await testData.getConfig(); - delete authFields.isDevHub; - - authInfoStub = stubInterface($$.SANDBOX, { - getFields: () => authFields, - }); - - stubMethod($$.SANDBOX, DeviceOauthService.prototype, 'requestDeviceLogin').returns(Promise.resolve(mockAction)); - - if (options.approvalFails) { - stubMethod($$.SANDBOX, DeviceOauthService.prototype, 'awaitDeviceApproval').returns(Promise.resolve()); - } - - if (options.approvalTimesout) { - stubMethod($$.SANDBOX, DeviceOauthService.prototype, 'pollForDeviceApproval').throws('polling timeout'); - } else { - stubMethod($$.SANDBOX, DeviceOauthService.prototype, 'pollForDeviceApproval').resolves({ - access_token: '1234', - refresh_token: '1234', - signature: '1234', - scope: '1234', - instance_url: 'https://login.salesforce.com', - id: '1234', - token_type: '1234', - issued_at: '1234', - }); - } - - stubMethod($$.SANDBOX, AuthInfo, 'create').resolves(authInfoStub); - await $$.stubAuths(testData); - stubUx($$.SANDBOX); - stubMethod($$.SANDBOX, SfCommand.prototype, 'logSuccess'); - } - - it('should return auth fields', async () => { - await prepareStubs(); - const response = await Login.run(['--json']); - expect(response.username).to.equal(testData.username); - }); - - it('should return auth fields with instance url', async () => { - await prepareStubs(); - const response = await Login.run(['-r', 'https://login.salesforce.com', '--json']); - expect(response.username).to.equal(testData.username); - }); - - it('should set alias when -a is provided', async () => { - await prepareStubs(); - const response = await Login.run(['-a', 'MyAlias', '--json']); - expect(response.username).to.equal(testData.username); - expect(authInfoStub.handleAliasAndDefaultSettings.callCount).to.equal(1); - }); - - it('should set target-org when -s is provided', async () => { - await prepareStubs(); - const response = await Login.run(['-s', '--json']); - expect(response.username).to.equal(testData.username); - expect(authInfoStub.handleAliasAndDefaultSettings.callCount).to.equal(1); - }); - - it('should set target-dev-hub when -d is provided', async () => { - await prepareStubs(); - const response = await Login.run(['-d', '--json']); - expect(response.username).to.equal(testData.username); - expect(authInfoStub.handleAliasAndDefaultSettings.callCount).to.equal(1); - }); - - it('show required action in human readable output', async () => { - await prepareStubs(); - const styledHeaderSpy = $$.SANDBOX.spy(SfCommand.prototype, 'styledHeader'); - const logSpy = $$.SANDBOX.spy(SfCommand.prototype, 'log'); - await Login.run([]); - expect(styledHeaderSpy.calledOnce).to.be.true; - expect(logSpy.callCount).to.be.greaterThan(0); - }); - - it('should gracefully handle approval timeout', async () => { - await prepareStubs({ approvalTimesout: true }); - try { - const response = await Login.run(['--json']); - expect.fail(`should have thrown: ${JSON.stringify(response)}`); - } catch (e) { - expect((e as Error).name).to.equal('polling timeout'); - } - }); - - it('should gracefully handle failed approval', async () => { - await prepareStubs({ approvalFails: true }); - const response = await Login.run(['--json']); - expect(response).to.deep.equal({}); - }); - - it('should prompt for client secret if client id is provided', async () => { - await prepareStubs(); - $$.SANDBOX.stub(SfCommand.prototype, 'secretPrompt').resolves('1234'); - const response = await Login.run(['-i', 'CoffeeBeans', '--json']); - expect(response.username).to.equal(testData.username); - }); -});