Skip to content

Commit 10fb8dd

Browse files
committed
Fix Updated the totp to mfa and added support for env variable
1 parent cddf127 commit 10fb8dd

22 files changed

Lines changed: 867 additions & 689 deletions

File tree

.talismanrc

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
fileignoreconfig:
2-
- filename: package-lock.json
3-
checksum: ddcdc0f28b1df533e26c370f92810f1c877aae48ed2157f8822a1f275adad612
4-
- filename: pnpm-lock.yaml
5-
checksum: c32024bc35de63368636624ef52c1b0cf6c4e1dbcfa93ced09e57f6e8ca454ca
62
- filename: packages/contentstack-import-setup/test/unit/backup-handler.test.ts
73
checksum: 0582d62b88834554cf12951c8690a73ef3ddbb78b82d2804d994cf4148e1ef93
84
- filename: packages/contentstack-import-setup/test/config.json
@@ -46,5 +42,31 @@ fileignoreconfig:
4642
- filename: packages/contentstack-auth/test/integration/auth.test.ts
4743
checksum: 96a66c141cf8f83443f967f62be210c3a95e06cf3d6c7bcb25229a4de7f05c5f
4844
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
49-
checksum: c256cb00cbe8a5f2ded2907677f7a55b4661cd95f1145d7bbd10740702e10e5c
45+
checksum: e8a1e413008e19de3c35cbf85d0d8433f0ac25ddf89d9a5cdd118bbc875321b9
46+
- filename: packages/contentstack-auth/env.example
47+
checksum: 72c9ed18a449c42b03ec54795898f6bad4e15d23a3d701c05b96fb17c3bbd93b
48+
- filename: packages/contentstack-auth/test/unit/utils/mfa-handler.test.ts
49+
checksum: 741d0072dbe79d0d0e8ba08c49c796cb31782993a91a0493c399021106b17d72
50+
- filename: packages/contentstack-config/src/commands/config/mfa/add.ts
51+
checksum: db0e4de369f1c08aa1061aa08b3561748e5a87de7fbaf37f8fed49afa2471114
52+
- filename: packages/contentstack-config/src/services/mfa/mfa.service.ts
53+
checksum: c0bf969154a243036c402194d63ada2ada5efa6c5843674293de25f49292ea5b
54+
- filename: packages/contentstack-auth/src/utils/mfa-handler.ts
55+
checksum: 2b813050da41744bda53b0c97617fb5beb0b370c148792a7d21bc4dd4bbae18f
56+
- filename: packages/contentstack-auth/messages/index.json
57+
checksum: 17bc512822ad037c5aaa0439bc6d516511bab0ce9b6153fc923a991579ac9550
58+
- filename: packages/contentstack-config/test/unit/commands/mfa.test.ts
59+
checksum: 444312de89cc9f70647ec23e2322415cc8eb48e4e43456396d123650308680bd
60+
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
61+
checksum: e8a1e413008e19de3c35cbf85d0d8433f0ac25ddf89d9a5cdd118bbc875321b9
62+
- filename: package-lock.json
63+
checksum: 6d72f17fc014790bbc82bcae559d2b49c42c416c59ad15979af028a27275fe59
64+
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
65+
checksum: e8a1e413008e19de3c35cbf85d0d8433f0ac25ddf89d9a5cdd118bbc875321b9
66+
- filename: packages/contentstack/README.md
67+
checksum: f82a59b23959a82b0172663fab0aa9d65b16fbcb256652b59f12227b8d4c1d03
68+
- filename: pnpm-lock.yaml
69+
checksum: c300f5c7b5ebe755ef55765331446e619c9efd6f6f18d89a31017594a39acbe6
70+
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
71+
checksum: e8a1e413008e19de3c35cbf85d0d8433f0ac25ddf89d9a5cdd118bbc875321b9
5072
version: "1.0"

package-lock.json

Lines changed: 113 additions & 113 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/contentstack-auth/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ $ npm install -g @contentstack/cli-auth
1818
$ csdx COMMAND
1919
running command...
2020
$ csdx (--version)
21-
@contentstack/cli-auth/1.5.1 darwin-arm64 node-v22.13.1
21+
@contentstack/cli-auth/1.6.0 darwin-arm64 node-v23.11.0
2222
$ csdx --help [COMMAND]
2323
USAGE
2424
$ csdx COMMAND

packages/contentstack-auth/env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ BRANCH_ENABLED_DELIVERY_TOKEN
66
BRANCH_DISABLED_DELIVERY_TOKEN
77
BRANCH_ENABLED_ENVIRONMENT
88
BRANCH_DISABLED_ENVIRONMENT
9+
CONTENTSTACK_MFA_SECRET

packages/contentstack-auth/messages/index.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,16 @@
4949
"CLI_AUTH_TOKENS_VALIDATION_INVALID_API_KEY": "Invalid api key",
5050
"CLI_AUTH_EXIT_PROCESS": "Exiting the process...",
5151
"CLI_SELECT_TOKEN_TYPE": "Select the type of token to add",
52-
"CLI_AUTH_ENTER_BRANCH": "Enter branch name"
52+
"CLI_AUTH_MFA_INVALID_SECRET": "Invalid MFA secret format. Please check your authentication setup.",
53+
"CLI_AUTH_MFA_GENERATION_FAILED": "Failed to generate MFA code. Please try again.",
54+
"CLI_AUTH_MFA_DECRYPT_FAILED": "Failed to decrypt stored MFA secret. Please try again.",
55+
"CLI_AUTH_MFA_INVALID_CODE": "Invalid authentication code format. Please enter a 6-digit code.",
56+
"CLI_AUTH_MFA_RECONFIGURE_HINT": "Consider reconfiguring MFA using config:mfa:add command.",
57+
"CLI_AUTH_SMS_OTP_FAILED": "Failed to send SMS OTP. Please try again or use a different 2FA method.",
58+
"CLI_AUTH_2FA_FAILED": "Two-factor authentication failed. Please try again.",
59+
"CLI_AUTH_LOGIN_NO_USER": "No user found with the provided credentials.",
60+
"CLI_AUTH_LOGIN_NO_CREDENTIALS": "No credentials provided for login. Please provide email and password.",
61+
"CLI_AUTH_LOGOUT_NO_TOKEN": "No auth token found for logout. Please login first.",
62+
"CLI_AUTH_TOKEN_VALIDATION_FAILED": "Token validation failed. Please login again.",
63+
"CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN": "No auth token found for validation. Please login first."
5364
}

packages/contentstack-auth/src/commands/auth/login.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
messageHandler,
1111
} from '@contentstack/cli-utilities';
1212
import { User } from '../../interfaces';
13-
import { authHandler, interactive, totpHandler } from '../../utils';
13+
import { authHandler, interactive, mfaHandler } from '../../utils';
1414
import { BaseCommand } from '../../base-command';
1515

1616
export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
@@ -101,10 +101,10 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
101101
let tfaToken: string | undefined;
102102

103103
try {
104-
tfaToken = await totpHandler.getTOTPCode();
105-
log.debug('TOTP token generated from stored configuration', this.contextDetails);
104+
tfaToken = await mfaHandler.getMFACode();
105+
log.debug('MFA token generated from stored configuration', this.contextDetails);
106106
} catch (error) {
107-
log.debug('Failed to generate TOTP token from config', { ...this.contextDetails, error });
107+
log.debug('Failed to generate MFA token from config', { ...this.contextDetails, error });
108108
tfaToken = undefined;
109109
}
110110

packages/contentstack-auth/src/utils/auth-handler.ts

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cliux, CLIError, log, cliErrorHandler } from '@contentstack/cli-utilities';
1+
import { cliux, log, handleAndLogError, messageHandler } from '@contentstack/cli-utilities';
22
import { User } from '../interfaces';
33
import { askOTPChannel, askOTP } from './interactive';
44

@@ -48,19 +48,18 @@ class AuthHandler {
4848
try {
4949
await this.requestSMSOTP(loginPayload);
5050
} catch (error) {
51-
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
52-
throw new CLIError('Failed to send SMS OTP. Please try again or use a different 2FA method.');
51+
log.error('SMS OTP request failed', { module: 'auth-handler', error });
52+
cliux.print('CLI_AUTH_SMS_OTP_FAILED', { color: 'yellow' });
53+
handleAndLogError(error, { module: 'auth-handler' });
5354
}
5455
}
5556

5657
log.debug('Requesting OTP input', { module: 'auth-handler', channel: otpChannel });
5758
return await askOTP();
5859
} catch (error) {
59-
log.debug('2FA flow failed', { module: 'auth-handler', error });
60-
if (error instanceof CLIError) {
61-
throw error;
62-
}
63-
throw new CLIError('Failed to complete 2FA authentication. Please try again.');
60+
log.error('2FA flow failed', { module: 'auth-handler', error });
61+
cliux.print('CLI_AUTH_2FA_FAILED', { color: 'yellow' });
62+
handleAndLogError(error, { module: 'auth-handler' });
6463
}
6564
}
6665

@@ -76,9 +75,9 @@ class AuthHandler {
7675
log.debug('SMS OTP request successful', { module: 'auth-handler' });
7776
cliux.print('CLI_AUTH_LOGIN_SECURITY_CODE_SEND_SUCCESS');
7877
} catch (error) {
79-
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
80-
const err = cliErrorHandler.classifyError(error);
81-
throw new CLIError(err);
78+
log.error('SMS OTP request failed', { module: 'auth-handler', error });
79+
handleAndLogError(error, { module: 'auth-handler' });
80+
throw error;
8281
}
8382
}
8483

@@ -130,19 +129,23 @@ class AuthHandler {
130129
resolve(await this.login(email, password, tfToken));
131130
} catch (error) {
132131
log.debug('Login with TFA token failed', { module: 'auth-handler', error });
133-
const err = cliErrorHandler.classifyError(error);
134-
reject(new CLIError(err));
132+
handleAndLogError(error, { module: 'auth-handler' });
133+
log.debug('2FA authentication failed', { module: 'auth-handler', error });
134+
cliux.print('CLI_AUTH_2FA_FAILED', { color: 'yellow' });
135+
handleAndLogError(error, { module: 'auth-handler' });
135136
return;
136137
}
137138
} else {
138139
log.debug('Login failed - no user found', { module: 'auth-handler', result });
139-
reject(new CLIError({ message: 'No user found with the credentials' }));
140+
log.debug('Login failed - no user found', { module: 'auth-handler', result });
141+
cliux.print('CLI_AUTH_LOGIN_NO_USER', { color: 'yellow' });
142+
handleAndLogError(new Error(messageHandler.parse('CLI_AUTH_LOGIN_NO_USER')), { module: 'auth-handler' });
140143
}
141144
})
142145
.catch((error: any) => {
143146
log.debug('Login API call failed', { module: 'auth-handler', error: error.message || error });
144-
const err = cliErrorHandler.classifyError(error);
145-
reject(new CLIError(err));
147+
cliux.print('CLI_AUTH_LOGIN_FAILED', { color: 'yellow' });
148+
handleAndLogError(error, { module: 'auth-handler' });
146149
});
147150
} else {
148151
const hasEmail = !!email;
@@ -152,7 +155,9 @@ class AuthHandler {
152155
hasEmail,
153156
hasCredentials,
154157
});
155-
reject(new CLIError('No credential found to login'));
158+
log.debug('Login failed - missing credentials', { module: 'auth-handler', hasEmail, hasCredentials });
159+
cliux.print('CLI_AUTH_LOGIN_NO_CREDENTIALS', { color: 'yellow' });
160+
handleAndLogError(new Error(messageHandler.parse('CLI_AUTH_LOGIN_NO_CREDENTIALS')), { module: 'auth-handler' });
156161
}
157162
});
158163
}
@@ -177,12 +182,13 @@ class AuthHandler {
177182
})
178183
.catch((error: Error) => {
179184
log.debug('Logout API call failed', { module: 'auth-handler', error: error.message });
180-
const err = cliErrorHandler.classifyError(error);
181-
reject(new CLIError(err));
185+
cliux.print('CLI_AUTH_LOGOUT_FAILED', { color: 'yellow' });
186+
handleAndLogError(error, { module: 'auth-handler' });
182187
});
183188
} else {
184189
log.debug('Logout failed - no auth token provided', { module: 'auth-handler' });
185-
reject(new CLIError('No auth token found to logout'));
190+
cliux.print('CLI_AUTH_LOGOUT_NO_TOKEN', { color: 'yellow' });
191+
handleAndLogError(new Error(messageHandler.parse('CLI_AUTH_LOGOUT_NO_TOKEN')), { module: 'auth-handler' });
186192
}
187193
});
188194
}
@@ -207,12 +213,15 @@ class AuthHandler {
207213
})
208214
.catch((error: Error) => {
209215
log.debug('Token validation failed', { module: 'auth-handler', error: error.message });
210-
const err = cliErrorHandler.classifyError(error);
211-
reject(new CLIError(err));
216+
cliux.print('CLI_AUTH_TOKEN_VALIDATION_FAILED', { color: 'yellow' });
217+
handleAndLogError(error, { module: 'auth-handler' });
212218
});
213219
} else {
214220
log.debug('Token validation failed - no auth token provided', { module: 'auth-handler' });
215-
reject(new CLIError('No auth token found to validate'));
221+
cliux.print('CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN', { color: 'yellow' });
222+
handleAndLogError(new Error(messageHandler.parse('CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN')), {
223+
module: 'auth-handler',
224+
});
216225
}
217226
});
218227
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export { default as authHandler } from './auth-handler';
2-
export { default as totpHandler } from './totp-handler';
2+
export { default as mfaHandler } from './mfa-handler';
33
export * as interactive from './interactive';
44
export * as tokenValidation from './tokens-validation';
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { cliux, configHandler, NodeCrypto, log, handleAndLogError, messageHandler } from '@contentstack/cli-utilities';
2+
import { authenticator } from 'otplib';
3+
import { askOTP } from './interactive';
4+
5+
/**
6+
* @class
7+
* MFA handler for managing multi-factor authentication
8+
*/
9+
class MFAHandler {
10+
private readonly encrypter: NodeCrypto;
11+
12+
constructor() {
13+
this.encrypter = new NodeCrypto();
14+
}
15+
16+
/**
17+
* Validates if a string is a valid base32 secret
18+
* @param secret The secret to validate
19+
* @returns true if valid, false otherwise
20+
*/
21+
private isValidBase32(secret: string): boolean {
22+
// Base32 string must:
23+
// 1. Contain only uppercase letters A-Z and digits 2-7
24+
// 2. Be at least 16 characters long (before padding)
25+
// 3. Have valid padding (no single = character)
26+
const base32Regex = /^[A-Z2-7]+(?:={2,6})?$/;
27+
const nonPaddedLength = secret.replace(/=+$/, '').length;
28+
return base32Regex.test(secret) && nonPaddedLength >= 16;
29+
}
30+
31+
/**
32+
* Generates an MFA code from a provided secret
33+
* @param secret The MFA secret to use
34+
* @returns string The generated MFA code
35+
* @throws Error if the secret is invalid or code generation fails
36+
*/
37+
generateMFACode(secret: string): string {
38+
log.debug('Generating MFA code from provided secret', { module: 'mfa-handler' });
39+
40+
try {
41+
// Validate and normalize secret
42+
const normalizedSecret = secret.toUpperCase();
43+
if (!this.isValidBase32(normalizedSecret)) {
44+
log.debug('Invalid MFA secret format', { module: 'mfa-handler' });
45+
cliux.print('CLI_AUTH_MFA_INVALID_SECRET', { color: 'yellow' });
46+
throw new Error(messageHandler.parse('CLI_AUTH_MFA_INVALID_SECRET'));
47+
}
48+
49+
// Generate MFA code
50+
const code = authenticator.generate(normalizedSecret);
51+
log.debug('Generated MFA code successfully', { module: 'mfa-handler' });
52+
return code;
53+
} catch (error) {
54+
log.debug('Failed to generate MFA code', { module: 'mfa-handler', error });
55+
cliux.print('CLI_AUTH_MFA_GENERATION_FAILED', { color: 'yellow' });
56+
throw new Error(messageHandler.parse('CLI_AUTH_MFA_GENERATION_FAILED'));
57+
}
58+
}
59+
60+
/**
61+
* Gets MFA code from stored configuration
62+
* @returns Promise<string> The MFA code
63+
* @throws Error if MFA code generation fails
64+
*/
65+
async getMFACode(): Promise<string> {
66+
log.debug('Getting MFA code', { module: 'mfa-handler' });
67+
let secret: string | undefined;
68+
let source: string;
69+
70+
const envSecret = process.env.CONTENTSTACK_MFA_SECRET;
71+
if (envSecret) {
72+
log.debug('Found MFA secret in environment variable', { module: 'mfa-handler' });
73+
secret = envSecret;
74+
source = 'environment variable';
75+
}
76+
77+
if (!secret) {
78+
log.debug('Checking stored MFA secret', { module: 'mfa-handler' });
79+
const mfaConfig = configHandler.get('mfa');
80+
if (mfaConfig?.secret) {
81+
try {
82+
secret = this.encrypter.decrypt(mfaConfig.secret);
83+
source = 'stored configuration';
84+
} catch (error) {
85+
log.debug('Failed to decrypt stored MFA secret', { module: 'mfa-handler', error });
86+
cliux.print('CLI_AUTH_MFA_DECRYPT_FAILED', { color: 'yellow' });
87+
handleAndLogError(new Error(messageHandler.parse('CLI_AUTH_MFA_DECRYPT_FAILED')), { module: 'mfa-handler' });
88+
}
89+
}
90+
}
91+
92+
if (secret) {
93+
try {
94+
const code = this.generateMFACode(secret);
95+
log.debug('Generated MFA code', { module: 'mfa-handler', source });
96+
return code;
97+
} catch (error) {
98+
log.debug('Failed to generate MFA code', { module: 'mfa-handler', error, source });
99+
cliux.print('CLI_AUTH_MFA_GENERATION_FAILED', { color: 'yellow' });
100+
cliux.print('CLI_AUTH_MFA_RECONFIGURE_HINT');
101+
handleAndLogError(new Error(messageHandler.parse('CLI_AUTH_MFA_GENERATION_FAILED')), { module: 'mfa-handler' });
102+
}
103+
}
104+
105+
// No secret available, ask for manual input
106+
log.debug('No MFA secret found, requesting manual input', { module: 'mfa-handler' });
107+
return this.getManualMFACode();
108+
}
109+
110+
/**
111+
* Gets MFA code through manual user input
112+
* @returns Promise<string> The MFA code
113+
* @throws Error if code format is invalid
114+
*/
115+
async getManualMFACode(): Promise<string> {
116+
const code = await askOTP();
117+
if (!/^\d{6}$/.test(code)) {
118+
log.debug('Invalid MFA code format', { module: 'mfa-handler', code });
119+
cliux.print('CLI_AUTH_MFA_INVALID_CODE', { color: 'yellow' });
120+
handleAndLogError(new Error(messageHandler.parse('CLI_AUTH_MFA_INVALID_CODE')), { module: 'mfa-handler' });
121+
}
122+
return code;
123+
}
124+
125+
/**
126+
* Validates an MFA code format
127+
* @param code The MFA code to validate
128+
* @returns boolean True if valid, false otherwise
129+
*/
130+
isValidMFACode(code: string): boolean {
131+
return /^\d{6}$/.test(code);
132+
}
133+
134+
/**
135+
* Handles MFA authentication flow
136+
* @returns Promise<string> The valid MFA code
137+
*/
138+
async handleMFAAuth(): Promise<string> {
139+
try {
140+
return await this.getMFACode();
141+
} catch (error) {
142+
log.debug('MFA code generation failed, falling back to manual input', { module: 'mfa-handler', error });
143+
handleAndLogError(error, { module: 'mfa-handler' });
144+
return this.getManualMFACode();
145+
}
146+
}
147+
}
148+
149+
export default new MFAHandler();
150+

0 commit comments

Comments
 (0)