diff --git a/docs/docs/_clisettings.mdx b/docs/docs/_clisettings.mdx index 88be031491d..5f8a21ebd12 100644 --- a/docs/docs/_clisettings.mdx +++ b/docs/docs/_clisettings.mdx @@ -1,5 +1,6 @@ Setting name|Definition|Default value ------------|----------|------------- +`authType`|Default login method to use when running `m365 login` without the `--authType` option.|`deviceCode` `autoOpenLinksInBrowser`|Automatically open the browser for all commands which return a url and expect the user to copy paste this to the browser. For example when logging in, using `m365 login` in device code mode.|`false` `copyDeviceCodeToClipboard`|Automatically copy the device code to the clipboard when running `m365 login` command in device code mode|`false` `csvEscape`|Single character used for escaping; only apply to characters matching the quote and the escape options|`"` diff --git a/docs/docs/cmd/login.mdx b/docs/docs/cmd/login.mdx index 911c1446e17..775460236e2 100644 --- a/docs/docs/cmd/login.mdx +++ b/docs/docs/cmd/login.mdx @@ -52,7 +52,7 @@ m365 login [options] Using the `login` command you can log in to Microsoft 365. -By default, the `login` command uses device code OAuth flow to log in to Microsoft 365. Alternatively, you can authenticate using a user name and password or certificate, which are convenient for CI/CD scenarios, but which come with their own [limitations](../user-guide/connecting-microsoft-365.mdx). +By default, the `login` command uses device code OAuth flow to log in to Microsoft 365. Alternatively, you can authenticate using a user name and password or certificate, which are convenient for CI/CD scenarios, but which come with their own [limitations](../user-guide/connecting-microsoft-365.mdx). The default `authType` can be configured using `m365 cli config set`. This means you'll be able to run `m365 login` without specifying the `--authType` option. When logging in to Microsoft 365 using the user name and password, next to the access and refresh token, the CLI for Microsoft 365 will store the user credentials so that it can automatically re-authenticate if necessary. Similarly to the tokens, the credentials are removed by re-authenticating using the device code or by calling the [logout](logout.mdx) command. diff --git a/src/Auth.ts b/src/Auth.ts index 18e66b9045d..e67095742f9 100644 --- a/src/Auth.ts +++ b/src/Auth.ts @@ -423,7 +423,7 @@ export class Auth { const cli = Cli.getInstance(); cli.spinner.text = response.message; cli.spinner.spinner = { - frames: ['🌶️'] + frames: ['🌶️ '] }; // don't show spinner if running tests diff --git a/src/m365/cli/commands/config/config-set.spec.ts b/src/m365/cli/commands/config/config-set.spec.ts index 0e731bbc82e..72542bc7379 100644 --- a/src/m365/cli/commands/config/config-set.spec.ts +++ b/src/m365/cli/commands/config/config-set.spec.ts @@ -219,6 +219,41 @@ describe(commands.CONFIG_SET, () => { assert.strictEqual(actual, true); }); + it('fails validation if specified authType is invalid', async () => { + const actual = await command.validate({ options: { key: settingsNames.authType, value: 'invalid' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation for authType type deviceCode', async () => { + const actual = await command.validate({ options: { key: settingsNames.authType, value: 'deviceCode' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for authType type browser', async () => { + const actual = await command.validate({ options: { key: settingsNames.authType, value: 'browser' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for authType type certificate', async () => { + const actual = await command.validate({ options: { key: settingsNames.authType, value: 'certificate' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for authType type password', async () => { + const actual = await command.validate({ options: { key: settingsNames.authType, value: 'password' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for authType type identity', async () => { + const actual = await command.validate({ options: { key: settingsNames.authType, value: 'identity' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for authType type secret', async () => { + const actual = await command.validate({ options: { key: settingsNames.authType, value: 'secret' } }, commandInfo); + assert.strictEqual(actual, true); + }); + it('fails validation if specified error output type is invalid', async () => { const actual = await command.validate({ options: { key: settingsNames.errorOutput, value: 'invalid' } }, commandInfo); assert.notStrictEqual(actual, true); diff --git a/src/m365/cli/commands/config/config-set.ts b/src/m365/cli/commands/config/config-set.ts index 7254fdb2a2d..cb49156f4a9 100644 --- a/src/m365/cli/commands/config/config-set.ts +++ b/src/m365/cli/commands/config/config-set.ts @@ -75,6 +75,12 @@ class CliConfigSetCommand extends AnonymousCommand { return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${Cli.helpModes.join(', ')}`; } + const allowedAuthTypes = ['certificate', 'deviceCode', 'password', 'identity', 'browser', 'secret']; + if (args.options.key === settingsNames.authType && + allowedAuthTypes.indexOf(args.options.value) === -1) { + return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${allowedAuthTypes.join(', ')}`; + } + return true; } ); diff --git a/src/m365/commands/login.ts b/src/m365/commands/login.ts index 63329a58392..f21c773fc1f 100644 --- a/src/m365/commands/login.ts +++ b/src/m365/commands/login.ts @@ -9,6 +9,8 @@ import GlobalOptions from '../../GlobalOptions.js'; import { accessToken } from '../../utils/accessToken.js'; import { misc } from '../../utils/misc.js'; import commands from './commands.js'; +import { settingsNames } from '../../settingsNames.js'; +import { Cli } from '../../cli/Cli.js'; interface CommandArgs { options: Options; @@ -49,7 +51,7 @@ class LoginCommand extends Command { #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { - authType: args.options.authType || 'deviceCode', + authType: args.options.authType || Cli.getInstance().getSettingWithDefaultValue(settingsNames.authType, 'deviceCode'), cloud: args.options.cloud ?? CloudType.Public }); }); @@ -95,7 +97,9 @@ class LoginCommand extends Command { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (args.options.authType === 'password') { + const authType = args.options.authType || Cli.getInstance().getSettingWithDefaultValue(settingsNames.authType, 'deviceCode'); + + if (authType === 'password') { if (!args.options.userName) { return 'Required option userName missing'; } @@ -105,7 +109,7 @@ class LoginCommand extends Command { } } - if (args.options.authType === 'certificate') { + if (authType === 'certificate') { if (args.options.certificateFile && args.options.certificateBase64Encoded) { return 'Specify either certificateFile or certificateBase64Encoded, but not both.'; } @@ -121,12 +125,12 @@ class LoginCommand extends Command { } } - if (args.options.authType && - LoginCommand.allowedAuthTypes.indexOf(args.options.authType) < 0) { - return `'${args.options.authType}' is not a valid authentication type. Allowed authentication types are ${LoginCommand.allowedAuthTypes.join(', ')}`; + if (authType && + LoginCommand.allowedAuthTypes.indexOf(authType) < 0) { + return `'${authType}' is not a valid authentication type. Allowed authentication types are ${LoginCommand.allowedAuthTypes.join(', ')}`; } - if (args.options.authType === 'secret') { + if (authType === 'secret') { if (!args.options.secret) { return 'Required option secret missing'; } @@ -155,10 +159,11 @@ class LoginCommand extends Command { await logger.logToStderr(`Signing in to Microsoft 365...`); } + const authType = args.options.authType || Cli.getInstance().getSettingWithDefaultValue(settingsNames.authType, 'deviceCode'); auth.service.appId = args.options.appId || config.cliAadAppId; auth.service.tenant = args.options.tenant || config.tenant; - switch (args.options.authType) { + switch (authType) { case 'password': auth.service.authType = AuthType.Password; auth.service.userName = args.options.userName; diff --git a/src/settingsNames.ts b/src/settingsNames.ts index 41a29c14be9..51f16817f99 100644 --- a/src/settingsNames.ts +++ b/src/settingsNames.ts @@ -1,4 +1,5 @@ const settingsNames = { + authType: 'authType', autoOpenLinksInBrowser: 'autoOpenLinksInBrowser', copyDeviceCodeToClipboard: 'copyDeviceCodeToClipboard', csvEscape: 'csvEscape',