Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
cde7c35
feat: Added TOTP support login command and version bump
cs-raj Aug 7, 2025
1b953e5
Create .snyk
kaustubh-CS Aug 8, 2025
5e664b9
Create .snyk
kaustubh-CS Aug 8, 2025
4c3d0ff
Create .snyk
kaustubh-CS Aug 8, 2025
6a57e43
Create .snyk
kaustubh-CS Aug 8, 2025
fce71e7
Create .snyk
kaustubh-CS Aug 8, 2025
4dab3cc
Create .snyk
kaustubh-CS Aug 8, 2025
50cc971
Create .snyk
kaustubh-CS Aug 8, 2025
ffa38d3
Update .snyk
kaustubh-CS Aug 8, 2025
aa4b61f
Update .snyk
kaustubh-CS Aug 8, 2025
b71ad32
Update .snyk
kaustubh-CS Aug 8, 2025
ba2aacc
Update .snyk
kaustubh-CS Aug 8, 2025
1f66eb4
Update .snyk
kaustubh-CS Aug 8, 2025
2410d51
Update .snyk
kaustubh-CS Aug 8, 2025
79c96f1
Update .snyk
kaustubh-CS Aug 8, 2025
a82d9c1
Resolved the PR comments
cs-raj Aug 8, 2025
faedd30
Added Try catch
cs-raj Aug 8, 2025
87b0ade
Fixed the
cs-raj Aug 8, 2025
75cfeaf
PR comment fix
cs-raj Aug 8, 2025
640125a
Feat: Added Config command to set the totp
cs-raj Aug 11, 2025
e0554e5
Adding last_updated
cs-raj Aug 11, 2025
6da4240
Merge branch 'staging' into feat/DX-3412
cs-raj Aug 11, 2025
4179fd2
Merge branch 'feat/DX-3412' into fix/DX-3413
cs-raj Aug 11, 2025
10117bb
Lock-file update
cs-raj Aug 11, 2025
c2b4723
Fixed Test cases
cs-raj Aug 11, 2025
f539bfb
Fixed the test cases
cs-raj Aug 11, 2025
d3f2a47
Changed the totp to MFA
cs-raj Aug 12, 2025
af47903
Updated talismand and command shortcut name
cs-raj Aug 12, 2025
a46f34d
Readme update
cs-raj Aug 12, 2025
e64af71
Tests updated
cs-raj Aug 12, 2025
2b59ba6
Integration Test fixes and unit test fixes
cs-raj Aug 12, 2025
6ba4223
Fixed the stub
cs-raj Aug 12, 2025
20ec490
Merge pull request #2058 from contentstack/fix/DX-3413
cs-raj Aug 12, 2025
cddf127
Merge branch 'development' into feat/DX-3412
cs-raj Aug 12, 2025
10fb8dd
Fix Updated the totp to mfa and added support for env variable
cs-raj Aug 13, 2025
8933eb6
Updated the config to remove the MFA Error handler and updated the lo…
cs-raj Aug 13, 2025
fe2ff14
Fixed the test cases and removed repeated code
cs-raj Aug 13, 2025
814bb82
Changed the condition for secret addition in config
cs-raj Aug 13, 2025
60d739c
Fixed Linting error
cs-raj Aug 13, 2025
b8b7865
Removed the data from the logs
cs-raj Aug 13, 2025
0f1e691
Fixed the PR comment
cs-raj Aug 14, 2025
a985a40
Ui Text updated
cs-raj Aug 14, 2025
d81b616
Removing the MFA secret if the region set command is fired
cs-raj Aug 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
fileignoreconfig:
- filename: package-lock.json
checksum: c73d080bfbbf03bc2597891f956367bfd71f5f2f8d03525175426015265edc91
- filename: pnpm-lock.yaml
checksum: 0ca85066946c49994a4353c9f64b8f380d5d2050194e3e57ad7ccd7faa030d36
- filename: packages/contentstack-import-setup/test/unit/backup-handler.test.ts
checksum: 0582d62b88834554cf12951c8690a73ef3ddbb78b82d2804d994cf4148e1ef93
- filename: packages/contentstack-import-setup/test/config.json
Expand Down Expand Up @@ -35,4 +31,52 @@ fileignoreconfig:
checksum: c09f6dc93702caff3adf689b501ec32586a16c865c1fe3a63b53ae645ca22349
- filename: packages/contentstack-import-setup/test/unit/modules/assets.test.ts
checksum: 449a5e3383631a6f78d1291aa3c28c91681879289398f0a933158fba5c5d5acf
version: "1.0"
- filename: packages/contentstack-config/src/services/mfa/types.ts
checksum: 2817710204fc907642803e514bb51df506f60a196d00548362e7178a3bf21208
- filename: packages/contentstack-config/src/services/mfa/mfa-service.interface.ts
checksum: 68158e62e4e5f6d51538bed0789074a4f595f1e4b3a37e82edce6afe5b69bc30
- filename: packages/contentstack-config/test/unit/services/mfa.service.test.ts
checksum: 8ba652904813cc27d5be3c7829588c3f4b0a3b3fab50439676690fe95a1d4733
- filename: packages/contentstack-config/README.md
checksum: b560bf300a07b40d9c24534c8e3493b7569233de061cfcdd58eb615c96e83d75
- filename: packages/contentstack-auth/test/integration/auth.test.ts
checksum: 96a66c141cf8f83443f967f62be210c3a95e06cf3d6c7bcb25229a4de7f05c5f
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
checksum: e8a1e413008e19de3c35cbf85d0d8433f0ac25ddf89d9a5cdd118bbc875321b9
- filename: packages/contentstack-auth/env.example
checksum: 72c9ed18a449c42b03ec54795898f6bad4e15d23a3d701c05b96fb17c3bbd93b
- filename: packages/contentstack-auth/test/unit/utils/mfa-handler.test.ts
checksum: 15180df9adf6dac871af82d6983ad27ee75e053240396c88f172dda3fb92fe5a
- filename: packages/contentstack-config/src/commands/config/mfa/add.ts
checksum: 8e1648f18ac4dd8b5596a893ac247f355f00910de4203fcbd1c0895ec1e57344
- filename: packages/contentstack-config/src/services/mfa/mfa.service.ts
checksum: a77014d39e7380fbc677524f75b812b39601cf474312a38d4dc88409e440fc20
- filename: packages/contentstack-auth/src/utils/mfa-handler.ts
checksum: 2b813050da41744bda53b0c97617fb5beb0b370c148792a7d21bc4dd4bbae18f
- filename: packages/contentstack-auth/messages/index.json
checksum: 17bc512822ad037c5aaa0439bc6d516511bab0ce9b6153fc923a991579ac9550
- filename: packages/contentstack-config/test/unit/commands/mfa.test.ts
checksum: 444312de89cc9f70647ec23e2322415cc8eb48e4e43456396d123650308680bd
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
checksum: e8a1e413008e19de3c35cbf85d0d8433f0ac25ddf89d9a5cdd118bbc875321b9
- filename: package-lock.json
checksum: 92225c3f9d9fff053189faf70d507e99c4121a22ced09189e37fe2ce98c8469c
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
checksum: e8a1e413008e19de3c35cbf85d0d8433f0ac25ddf89d9a5cdd118bbc875321b9
- filename: packages/contentstack/README.md
checksum: f82a59b23959a82b0172663fab0aa9d65b16fbcb256652b59f12227b8d4c1d03
- filename: pnpm-lock.yaml
checksum: c300f5c7b5ebe755ef55765331446e619c9efd6f6f18d89a31017594a39acbe6
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
checksum: e8a1e413008e19de3c35cbf85d0d8433f0ac25ddf89d9a5cdd118bbc875321b9
- filename: packages/contentstack-auth/src/commands/auth/login.ts
checksum: 2725fcbfd69f9290182073a362b0c2fa7de625fe6d2e0b0aa732d42678966179
- filename: packages/contentstack-auth/src/utils/mfa-handler.ts
checksum: a61790bd55c565cd56e5ad146816ab0a760f7ccc90c565cf7dff66a97ccef6e1
- filename: packages/contentstack-config/test/unit/commands/mfa.test.ts
checksum: 242491d40c7178132dc60abf3a251254559a002a9377b3038bc4a8b0c4708565
- filename: packages/contentstack-config/src/utils/interactive.ts
checksum: 740d954029fb600974ddcf586fe2da8b3619be99e5da3b1fa233cbe1c2d4d8fd
- filename: packages/contentstack-config/src/commands/config/mfa/remove.ts
checksum: 369e5cb85000beee3f18b0ce303a0301e192be1c0e554cd74b651c2d79db1cbb
version: ""
2,852 changes: 1,348 additions & 1,504 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/contentstack-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ $ npm install -g @contentstack/cli-auth
$ csdx COMMAND
running command...
$ csdx (--version)
@contentstack/cli-auth/1.5.1 darwin-arm64 node-v22.13.1
@contentstack/cli-auth/1.6.0 darwin-arm64 node-v23.11.0
$ csdx --help [COMMAND]
USAGE
$ csdx COMMAND
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-auth/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ BRANCH_ENABLED_DELIVERY_TOKEN
BRANCH_DISABLED_DELIVERY_TOKEN
BRANCH_ENABLED_ENVIRONMENT
BRANCH_DISABLED_ENVIRONMENT
CONTENTSTACK_MFA_SECRET
13 changes: 12 additions & 1 deletion packages/contentstack-auth/messages/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,16 @@
"CLI_AUTH_TOKENS_VALIDATION_INVALID_API_KEY": "Invalid api key",
"CLI_AUTH_EXIT_PROCESS": "Exiting the process...",
"CLI_SELECT_TOKEN_TYPE": "Select the type of token to add",
"CLI_AUTH_ENTER_BRANCH": "Enter branch name"
"CLI_AUTH_MFA_INVALID_SECRET": "Invalid MFA secret format. Please check your authentication setup.",
"CLI_AUTH_MFA_GENERATION_FAILED": "Failed to generate MFA code. Proceeding for Manual MFA code input.",
"CLI_AUTH_MFA_DECRYPT_FAILED": "Failed to decrypt stored MFA secret. Try Resetting the MFA secret. Proceeding for Manual MFA code input.",
"CLI_AUTH_MFA_INVALID_CODE": "Invalid authentication code format. Please enter a 6-digit code.",
"CLI_AUTH_MFA_RECONFIGURE_HINT": "Consider reconfiguring MFA using config:mfa:add",
"CLI_AUTH_SMS_OTP_FAILED": "Failed to send SMS OTP. Please try again or use a different 2FA method.",
"CLI_AUTH_2FA_FAILED": "Two-factor authentication failed. Please try again.",
"CLI_AUTH_LOGIN_NO_USER": "No user found with the provided credentials.",
"CLI_AUTH_LOGIN_NO_CREDENTIALS": "No credentials provided for login. Please provide email and password.",
"CLI_AUTH_LOGOUT_NO_TOKEN": "No auth token found for logout. Please login first.",
"CLI_AUTH_TOKEN_VALIDATION_FAILED": "Token validation failed. Please login again.",
"CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN": "No auth token found for validation. Please login first."
}
7 changes: 4 additions & 3 deletions packages/contentstack-auth/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@contentstack/cli-auth",
"description": "Contentstack CLI plugin for authentication activities",
"version": "1.5.1",
"version": "1.6.0",
"author": "Contentstack",
"bugs": "https://github.com/contentstack/cli/issues",
"scripts": {
Expand All @@ -24,8 +24,9 @@
"dependencies": {
"@contentstack/cli-command": "~1.6.0",
"@contentstack/cli-utilities": "~1.13.1",
"@oclif/core": "^4.3.0",
"@oclif/plugin-help": "^6.2.28"
"@oclif/core": "4.3.0",
"@oclif/plugin-help": "^6.2.28",
"otplib": "^12.0.1"
},
"devDependencies": {
"@fancy-test/nock": "^0.1.1",
Expand Down
37 changes: 30 additions & 7 deletions packages/contentstack-auth/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
messageHandler,
} from '@contentstack/cli-utilities';
import { User } from '../../interfaces';
import { authHandler, interactive } from '../../utils';
import { authHandler, interactive, mfaHandler } from '../../utils';
import { BaseCommand } from '../../base-command';

export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
Expand Down Expand Up @@ -40,6 +40,7 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
required: false,
exclusive: ['oauth'],
}),

oauth: flags.boolean({
description: 'Enables single sign-on (SSO) in Contentstack CLI.',
required: false,
Expand All @@ -57,9 +58,9 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
log.debug('Initializing management API client', this.contextDetails);
const managementAPIClient = await managementSDKClient({ host: this.cmaHost, skipTokenValidity: true });
log.debug('Management API client initialized successfully', this.contextDetails);

const { flags: loginFlags } = await this.parse(LoginCommand);
log.debug('Token add flags parsed', {...this.contextDetails, flags: loginFlags});
log.debug('Token add flags parsed', { ...this.contextDetails, flags: loginFlags });

authHandler.client = managementAPIClient;
log.debug('Auth handler client set', this.contextDetails);
Expand All @@ -76,12 +77,22 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
log.debug('Starting basic authentication flow', this.contextDetails);
const username = loginFlags?.username || (await interactive.askUsername());
const password = loginFlags?.password || (await interactive.askPassword());
log.debug('Credentials obtained', { ...this.contextDetails, hasUsername: !!username, hasPassword: !!password });
log.debug('Credentials obtained', {
...this.contextDetails,
hasUsername: !!username,
hasPassword: !!password,
});

await this.login(username, password);
}
} catch (error) {
log.debug('Login command failed', { ...this.contextDetails, error });
cliux.error('CLI_AUTH_LOGIN_FAILED');
log.debug('Login command failed', {
...this.contextDetails,
error,
});
if ((error.message && error.message.includes('2FA')) || error.message.includes('MFA')) {
error.message = `${error.message}\nFor more information about MFA, visit: https://www.contentstack.com/docs/developers/security/multi-factor-authentication`;
}
handleAndLogError(error, { ...this.contextDetails });
process.exit();
}
Expand All @@ -92,7 +103,19 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {

try {
log.debug('Calling auth handler login', this.contextDetails);
const user: User = await authHandler.login(username, password);
let tfaToken: string | undefined;

try {
tfaToken = await mfaHandler.getMFACode();
if(tfaToken){
log.debug('MFA token generated from stored configuration', this.contextDetails);
}
} catch (error) {
log.debug('Failed to generate MFA token from config', { ...this.contextDetails, error });
tfaToken = undefined;
}

const user: User = await authHandler.login(username, password, tfaToken);
log.debug('Auth handler login completed', {
...this.contextDetails,
hasUser: !!user,
Expand Down
118 changes: 76 additions & 42 deletions packages/contentstack-auth/src/utils/auth-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cliux, CLIError, log, cliErrorHandler } from '@contentstack/cli-utilities';
import { cliux, log, handleAndLogError, messageHandler } from '@contentstack/cli-utilities';
import { User } from '../interfaces';
import { askOTPChannel, askOTP } from './interactive';

Expand Down Expand Up @@ -27,9 +27,62 @@ class AuthHandler {
* @returns {Promise} Promise object returns authtoken on success
* TBD: take out the otp implementation from login and create a new method/function to handle otp
*/
/**
* Handle the OTP flow for 2FA authentication
* @param tfaToken Optional pre-provided TFA token
* @param loginPayload Login payload containing user credentials
* @returns Promise<string> The TFA token to use for authentication
*/
private async handleOTPFlow(tfaToken?: string, loginPayload?: any): Promise<string> {
try {
if (tfaToken) {
log.info('Using provided TFA token', { module: 'auth-handler' });
return tfaToken;
}

log.debug('2FA required, requesting OTP channel', { module: 'auth-handler' });
const otpChannel = await askOTPChannel();
log.debug(`OTP channel selected: ${otpChannel}`, { module: 'auth-handler' });

if (otpChannel === 'sms') {
try {
await this.requestSMSOTP(loginPayload);
} catch (error) {
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
cliux.print('CLI_AUTH_SMS_OTP_FAILED', { color: 'red' });
throw error;
}
}

log.debug('Requesting OTP input', { module: 'auth-handler', channel: otpChannel });
return await askOTP();
} catch (error) {
log.debug('2FA flow failed', { module: 'auth-handler', error });
cliux.print('CLI_AUTH_2FA_FAILED', { color: 'red' });
throw error;
}
}

/**
* Request SMS OTP for 2FA authentication
* @param loginPayload Login payload containing user credentials
* @throws CLIError if SMS request fails
*/
private async requestSMSOTP(loginPayload: any): Promise<void> {
log.debug('Sending SMS OTP request', { module: 'auth-handler' });
try {
await this._client.axiosInstance.post('/user/request_token_sms', { user: loginPayload });
log.debug('SMS OTP request successful', { module: 'auth-handler' });
cliux.print('CLI_AUTH_LOGIN_SECURITY_CODE_SEND_SUCCESS');
} catch (error) {
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
throw error;
}
}

async login(email: string, password: string, tfaToken?: string): Promise<User> {
const hasCredentials = !!password;
const hasTfaToken = !!tfaToken;
const hasCredentials = typeof password === 'string' && password.length > 0;
const hasTfaToken = typeof tfaToken === 'string' && tfaToken.length > 0;
log.debug('Starting login process', {
module: 'auth-handler',
email,
Expand All @@ -49,11 +102,9 @@ class AuthHandler {
log.debug('Adding TFA token to login payload', { module: 'auth-handler' });
}

const hasCredentials = !!password;
const hasTfaTokenPresent = !!tfaToken;
log.debug('Making login API call', {
module: 'auth-handler',
payload: { email, hasCredentials, hasTfaTokenPresent },
payload: { email, hasCredentials, hasTfaToken },
});

this._client
Expand All @@ -69,46 +120,25 @@ class AuthHandler {
log.debug('Login successful, user found', { module: 'auth-handler', userEmail: result.user.email });
resolve(result.user as User);
} else if (result.error_code === 294) {
log.debug('TFA required, requesting OTP channel', { module: 'auth-handler' });
const otpChannel = await askOTPChannel();
log.debug(`OTP channel selected: ${otpChannel}`, { module: 'auth-handler' });

// need to send sms to the mobile
if (otpChannel === 'sms') {
log.debug('Sending SMS OTP request', { module: 'auth-handler' });
try {
await this._client.axiosInstance.post('/user/request_token_sms', { user: loginPayload });
log.debug('SMS OTP request successful', { module: 'auth-handler' });
cliux.print('CLI_AUTH_LOGIN_SECURITY_CODE_SEND_SUCCESS');
} catch (error) {
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
const err = cliErrorHandler.classifyError(error);
reject(err);
return;
}
}

log.debug('Requesting OTP input from user', { module: 'auth-handler' });
const tfToken = await askOTP();
log.debug('OTP received, retrying login', { module: 'auth-handler' });
const tfToken = await this.handleOTPFlow(tfaToken, loginPayload);

try {
resolve(await this.login(email, password, tfToken));
} catch (error) {
log.debug('Login with TFA token failed', { module: 'auth-handler', error });
const err = cliErrorHandler.classifyError(error);
reject(err);
return;
handleAndLogError(error, { module: 'auth-handler' });
cliux.print('CLI_AUTH_2FA_FAILED', { color: 'red' });
reject(error);
}
} else {
log.debug('Login failed - no user found', { module: 'auth-handler', result });
reject(new CLIError({ message: 'No user found with the credentials' }));
reject(new Error(messageHandler.parse('CLI_AUTH_LOGIN_NO_USER')));
}
})
.catch((error: any) => {
log.debug('Login API call failed', { module: 'auth-handler', error: error.message || error });
const err = cliErrorHandler.classifyError(error);
reject(err);
log.debug('Login API call failed', { module: 'auth-handler', error: error.errorMessage || error });
cliux.print('CLI_AUTH_LOGIN_FAILED', { color: 'yellow' });
handleAndLogError(error, { module: 'auth-handler' });
});
} else {
const hasEmail = !!email;
Expand All @@ -118,7 +148,8 @@ class AuthHandler {
hasEmail,
hasCredentials,
});
reject(new CLIError({ message: 'No credential found to login' }));
log.debug('Login failed - missing credentials', { module: 'auth-handler', hasEmail, hasCredentials });
reject(new Error(messageHandler.parse('CLI_AUTH_LOGIN_NO_CREDENTIALS')));
}
});
}
Expand All @@ -143,12 +174,14 @@ class AuthHandler {
})
.catch((error: Error) => {
log.debug('Logout API call failed', { module: 'auth-handler', error: error.message });
const err = cliErrorHandler.classifyError(error);
reject(err);
cliux.print('CLI_AUTH_LOGOUT_FAILED', { color: 'yellow' });
handleAndLogError(error, { module: 'auth-handler' });
reject(error);
});
} else {
log.debug('Logout failed - no auth token provided', { module: 'auth-handler' });
reject(new CLIError({ message: 'No auth token found to logout' }));
cliux.print('CLI_AUTH_LOGOUT_NO_TOKEN', { color: 'yellow' });
reject(new Error(messageHandler.parse('CLI_AUTH_LOGOUT_NO_TOKEN')));
}
});
}
Expand All @@ -173,12 +206,13 @@ class AuthHandler {
})
.catch((error: Error) => {
log.debug('Token validation failed', { module: 'auth-handler', error: error.message });
const err = cliErrorHandler.classifyError(error);
reject(err);
cliux.print('CLI_AUTH_TOKEN_VALIDATION_FAILED', { color: 'yellow' });
handleAndLogError(error, { module: 'auth-handler' });
});
} else {
log.debug('Token validation failed - no auth token provided', { module: 'auth-handler' });
reject(new CLIError({ message: 'No auth token found to validate' }));
cliux.print('CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN', { color: 'yellow' });
reject(new Error(messageHandler.parse('CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN')));
}
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-auth/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as authHandler } from './auth-handler';
export { default as mfaHandler } from './mfa-handler';
export * as interactive from './interactive';
export * as tokenValidation from './tokens-validation';
2 changes: 1 addition & 1 deletion packages/contentstack-auth/src/utils/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const askOTPChannel = async (): Promise<string> => {
name: 'otpChannel',
message: 'CLI_AUTH_LOGIN_ASK_CHANNEL_FOR_OTP',
choices: [
{ name: 'Authy App', value: 'authy' },
{ name: 'Authenticator App', value: 'authenticator_app' },
{ name: 'SMS', value: 'sms' },
],
});
Expand Down
Loading