Skip to content

Commit d3f2a47

Browse files
committed
Changed the totp to MFA
1 parent f539bfb commit d3f2a47

13 files changed

Lines changed: 431 additions & 466 deletions

File tree

.talismanrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,13 @@ fileignoreconfig:
4545
checksum: 63aeb5dc49a60c195a5cfcefd52aeb276f0bb5227152d44c7d6305b0dbf7bee5
4646
- filename: packages/contentstack-config/src/commands/config/totp/remove.ts
4747
checksum: 17f6576f90d3b30f37c5ef1318b188db2c70ab31e7fdedc3c94807d1eb822a8b
48+
- filename: packages/contentstack-config/src/services/mfa/types.ts
49+
checksum: 2817710204fc907642803e514bb51df506f60a196d00548362e7178a3bf21208
50+
- filename: packages/contentstack-config/src/services/mfa/mfa-service.interface.ts
51+
checksum: 68158e62e4e5f6d51538bed0789074a4f595f1e4b3a37e82edce6afe5b69bc30
52+
- filename: packages/contentstack-config/src/services/totp/types.ts
53+
checksum: 3aaf2a825de2e8876590518bb6a5ae8f964d5d33188cbd44c3a8555507424a32
54+
- filename: packages/contentstack-config/test/unit/services/mfa.service.test.ts
55+
checksum: 8ba652904813cc27d5be3c7829588c3f4b0a3b3fab50439676690fe95a1d4733
4856
version: "1.0"
4957

packages/contentstack-config/src/commands/config/totp/add.ts renamed to packages/contentstack-config/src/commands/config/mfa/add.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
import { cliux } from '@contentstack/cli-utilities';
22
import { Flags } from '@oclif/core';
33
import { BaseCommand } from '../../../base-command';
4-
import { TOTPService } from '../../../services/totp/totp.service';
5-
import { TOTPError } from '../../../services/totp/types';
4+
import { MFAService } from '../../../services/mfa/mfa.service';
5+
import { MFAError } from '../../../services/mfa/mfa.types';
66

7-
export default class AddTOTPCommand extends BaseCommand<typeof AddTOTPCommand> {
8-
static readonly description = 'Add TOTP secret for 2FA authentication';
7+
export default class AddMFACommand extends BaseCommand<typeof AddMFACommand> {
8+
static readonly description = 'Add MFA secret for 2FA authentication';
99

1010
static readonly examples = [
11-
'$ csdx config:totp:add',
11+
'$ csdx config:mfa:add',
1212
];
1313

14-
private readonly totpService: TOTPService;
14+
private readonly mfaService: MFAService;
1515

1616
constructor(argv: string[], config: any) {
1717
super(argv, config);
18-
this.totpService = new TOTPService();
18+
this.mfaService = new MFAService();
1919
}
2020

2121
static readonly flags = {
2222
secret: Flags.string({
23-
description: 'TOTP secret for 2FA authentication',
23+
description: 'MFA secret for 2FA authentication',
2424
required: false,
2525
}),
2626
};
2727

2828
async run(): Promise<void> {
2929
try {
30-
const { flags } = await this.parse(AddTOTPCommand);
30+
const { flags } = await this.parse(AddMFACommand);
3131
let secret = flags.secret;
3232

3333
if (!secret) {
@@ -37,19 +37,19 @@ export default class AddTOTPCommand extends BaseCommand<typeof AddTOTPCommand> {
3737
message: 'Enter your secret:',
3838
validate: (input: string) => {
3939
if (!input) return 'Secret is required';
40-
if (!this.totpService.validateSecret(input)) return 'Invalid TOTP secret format';
40+
if (!this.mfaService.validateSecret(input)) return 'Invalid secret format';
4141
return true;
4242
},
4343
});
4444
}
4545

4646
// Validate secret if provided via flag
47-
if (!secret || !this.totpService.validateSecret(secret)) {
48-
throw new TOTPError('Invalid TOTP secret format');
47+
if (!secret || !this.mfaService.validateSecret(secret)) {
48+
throw new MFAError('Invalid secret format');
4949
}
5050

51-
// Check if TOTP configuration already exists
52-
const existingConfig = this.totpService.getStoredConfig();
51+
// Check if MFA configuration already exists
52+
const existingConfig = this.mfaService.getStoredConfig();
5353
if (existingConfig) {
5454
const confirm = await cliux.inquire({
5555
type: 'confirm',
@@ -65,20 +65,20 @@ export default class AddTOTPCommand extends BaseCommand<typeof AddTOTPCommand> {
6565

6666
// Encrypt and store the secret
6767
try {
68-
const encryptedSecret = this.totpService.encryptSecret(secret);
69-
this.totpService.storeConfig({ secret: encryptedSecret });
68+
const encryptedSecret = this.mfaService.encryptSecret(secret);
69+
this.mfaService.storeConfig({ secret: encryptedSecret });
7070
cliux.success('Secret has been stored successfully');
7171
} catch (error) {
72-
if (error instanceof TOTPError) {
72+
if (error instanceof MFAError) {
7373
throw error;
7474
}
75-
throw new TOTPError('Failed to store configuration');
75+
throw new MFAError('Failed to store secret');
7676
}
7777
} catch (error) {
78-
if (error instanceof TOTPError) {
78+
if (error instanceof MFAError) {
7979
cliux.error(error.message);
8080
} else {
81-
cliux.error('Failed to store configuration');
81+
cliux.error('Failed to store secret');
8282
}
8383
throw error;
8484
}

packages/contentstack-config/src/commands/config/totp/remove.ts renamed to packages/contentstack-config/src/commands/config/mfa/remove.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { cliux } from '@contentstack/cli-utilities';
22
import { BaseCommand } from '../../../base-command';
3-
import { TOTPService } from '../../../services/totp/totp.service';
4-
import { TOTPError } from '../../../services/totp/types';
3+
import { MFAService } from '../../../services/mfa/mfa.service';
4+
import { MFAError } from '../../../services/mfa/mfa.types';
55
import { Flags } from '@oclif/core';
66

7-
export default class RemoveTOTPCommand extends BaseCommand<typeof RemoveTOTPCommand> {
7+
export default class RemoveMFACommand extends BaseCommand<typeof RemoveMFACommand> {
88
static readonly description = 'Remove stored secret';
99

1010
static readonly examples = [
11-
'$ csdx config:totp:remove',
12-
'$ csdx config:totp:remove -y',
11+
'$ csdx config:mfa:remove',
12+
'$ csdx config:mfa:remove -y',
1313
];
1414

1515
static readonly flags = {
@@ -20,34 +20,34 @@ export default class RemoveTOTPCommand extends BaseCommand<typeof RemoveTOTPComm
2020
}),
2121
};
2222

23-
private readonly totpService: TOTPService;
23+
private readonly mfaService: MFAService;
2424

2525
constructor(argv: string[], config: any) {
2626
super(argv, config);
27-
this.totpService = new TOTPService();
27+
this.mfaService = new MFAService();
2828
}
2929

3030
async run(): Promise<void> {
3131
try {
32-
const { flags } = await this.parse(RemoveTOTPCommand);
32+
const { flags } = await this.parse(RemoveMFACommand);
3333

3434
let config;
3535
try {
36-
config = this.totpService.getStoredConfig();
36+
config = this.mfaService.getStoredConfig();
3737
if (!config?.secret) {
38-
throw new TOTPError('Failed to remove configuration');
38+
throw new MFAError('Failed to remove secret configuration');
3939
}
4040
} catch (error) {
41-
if (error instanceof TOTPError) {
41+
if (error instanceof MFAError) {
4242
throw error;
4343
}
44-
throw new TOTPError('Failed to remove configuration');
44+
throw new MFAError('Failed to remove secret configuration');
4545
}
4646

4747
// Verify the configuration is valid
4848
let isCorrupted = false;
4949
try {
50-
this.totpService.decryptSecret(config.secret);
50+
this.mfaService.decryptSecret(config.secret);
5151
} catch (error) {
5252
this.logger.debug('Failed to decrypt secret', { error });
5353
isCorrupted = true;
@@ -73,17 +73,17 @@ export default class RemoveTOTPCommand extends BaseCommand<typeof RemoveTOTPComm
7373
}
7474

7575
try {
76-
this.totpService.removeConfig();
76+
this.mfaService.removeConfig();
7777
cliux.success('Secret has been removed successfully');
7878
} catch (error) {
7979
this.logger.error('Failed to remove secret configuration', { error });
80-
throw new TOTPError('Failed to remove configuration');
80+
throw new MFAError('Failed to remove secret configuration');
8181
}
8282
} catch (error) {
83-
if (error instanceof TOTPError) {
83+
if (error instanceof MFAError) {
8484
cliux.error(error.message);
8585
} else {
86-
cliux.error('Failed to remove configuration');
86+
cliux.error('Failed to remove secret configuration');
8787
}
8888
throw error;
8989
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { MFAConfig } from './mfa.types';
2+
3+
export interface IMFAService {
4+
validateSecret(secret: string): boolean;
5+
encryptSecret(secret: string): string;
6+
decryptSecret(encryptedSecret: string): string;
7+
getStoredConfig(): MFAConfig | null;
8+
storeConfig(config: Partial<MFAConfig>): void;
9+
removeConfig(): void;
10+
generateMFA(secret: string): string;
11+
verifyMFA(secret: string, token: string): boolean;
12+
}

packages/contentstack-config/src/services/totp/totp.service.ts renamed to packages/contentstack-config/src/services/mfa/mfa.service.ts

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { configHandler, NodeCrypto, log } from '@contentstack/cli-utilities';
22
import { authenticator } from 'otplib';
3-
import { TOTPConfig, TOTPError } from './totp.types';
4-
import { ITOTPService } from './totp-service.interface';
3+
import { MFAConfig, MFAError } from './mfa.types';
4+
import { IMFAService } from './mfa-service.interface';
55

6-
export class TOTPService implements ITOTPService {
6+
export class MFAService implements IMFAService {
77
private readonly encrypter: NodeCrypto;
88
private readonly logger = log;
99

@@ -28,15 +28,19 @@ export class TOTPService implements ITOTPService {
2828
return false;
2929
}
3030

31-
const paddingRegex = /=+$/;
31+
// Check padding - must be 0, 2, 4, 5, 6, or 7 equals signs
32+
const paddingRegex = /=*$/;
3233
const paddingMatch = paddingRegex.exec(normalizedSecret);
3334
if (paddingMatch) {
3435
const paddingLength = paddingMatch[0].length;
35-
const unpadded = normalizedSecret.slice(0, -paddingLength);
36-
if (paddingLength > 6 || unpadded.length % 8 !== 0) {
36+
if (paddingLength === 1 || paddingLength === 3 || paddingLength > 7) {
3737
return false;
3838
}
39-
} else if (normalizedSecret.length % 8 !== 0) {
39+
}
40+
41+
// Check that the length without padding is a multiple of 8
42+
const unpaddedLength = normalizedSecret.replace(/=+$/, '').length;
43+
if (unpaddedLength % 8 !== 0) {
4044
return false;
4145
}
4246

@@ -54,7 +58,7 @@ export class TOTPService implements ITOTPService {
5458
return this.encrypter.encrypt(secret.trim().toUpperCase());
5559
} catch (error) {
5660
this.logger.error('Secret encryption failed', { error });
57-
throw new TOTPError('Failed to encrypt secret');
61+
throw new MFAError('Failed to encrypt secret');
5862
}
5963
}
6064

@@ -63,53 +67,52 @@ export class TOTPService implements ITOTPService {
6367
return this.encrypter.decrypt(encryptedSecret);
6468
} catch (error) {
6569
this.logger.error('Secret decryption failed', { error });
66-
throw new TOTPError('Failed to decrypt secret');
70+
throw new MFAError('Failed to decrypt secret');
6771
}
6872
}
6973

70-
getStoredConfig(): TOTPConfig | null {
74+
getStoredConfig(): MFAConfig | null {
7175
try {
72-
const config = configHandler.get('totp');
73-
return config?.secret ? config as TOTPConfig : null;
76+
const config = configHandler.get('mfa');
77+
return config?.secret ? config as MFAConfig : null;
7478
} catch (error) {
7579
this.logger.error('Failed to read config', { error });
76-
throw new TOTPError('Failed to read configuration');
80+
throw new MFAError('Failed to read configuration');
7781
}
7882
}
7983

80-
storeConfig(config: Partial<TOTPConfig>): void {
84+
storeConfig(config: Partial<MFAConfig>): void {
8185
try {
82-
const updatedConfig: TOTPConfig = {
83-
...config,
84-
last_updated: new Date().toISOString(),
85-
secret: config.secret!
86+
const updatedConfig: MFAConfig = {
87+
secret: config.secret!,
88+
last_updated: new Date().toISOString()
8689
};
87-
configHandler.set('totp', updatedConfig);
90+
configHandler.set('mfa', updatedConfig);
8891
} catch (error) {
8992
this.logger.error('Failed to store config', { error });
90-
throw new TOTPError('Failed to store configuration');
93+
throw new MFAError('Failed to store configuration');
9194
}
9295
}
9396

9497
removeConfig(): void {
9598
try {
96-
configHandler.delete('totp');
99+
configHandler.delete('mfa');
97100
} catch (error) {
98101
this.logger.error('Failed to remove config', { error });
99-
throw new TOTPError('Failed to remove configuration');
102+
throw new MFAError('Failed to remove configuration');
100103
}
101104
}
102105

103-
generateTOTP(secret: string): string {
106+
generateMFA(secret: string): string {
104107
try {
105108
return authenticator.generate(secret.trim().toUpperCase());
106109
} catch (error) {
107110
this.logger.error('Failed to generate code', { error });
108-
throw new TOTPError('Failed to generate code');
111+
throw new MFAError('Failed to generate code');
109112
}
110113
}
111114

112-
verifyTOTP(secret: string, token: string): boolean {
115+
verifyMFA(secret: string, token: string): boolean {
113116
try {
114117
return authenticator.check(token, secret.trim().toUpperCase());
115118
} catch (error) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export interface MFAConfig {
2+
secret: string;
3+
last_updated: string;
4+
}
5+
6+
export interface MFAValidationResult {
7+
isValid: boolean;
8+
error?: string;
9+
}
10+
11+
export class MFAError extends Error {
12+
constructor(message: string) {
13+
super(message);
14+
this.name = 'MFAError';
15+
Object.setPrototypeOf(this, MFAError.prototype);
16+
}
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export interface MFAConfig {
2+
secret: string;
3+
last_updated: string;
4+
}
5+
6+
export interface MFAValidationResult {
7+
isValid: boolean;
8+
error?: string;
9+
}
10+
11+
export interface IMFAService {
12+
validateSecret(secret: string): boolean;
13+
encryptSecret(secret: string): string;
14+
decryptSecret(encryptedSecret: string): string;
15+
getStoredConfig(): MFAConfig | null;
16+
storeConfig(config: MFAConfig): void;
17+
removeConfig(): void;
18+
generateMFA(secret: string): string;
19+
verifyMFA(secret: string, token: string): boolean;
20+
}

packages/contentstack-config/src/services/totp/totp-service.interface.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

packages/contentstack-config/src/services/totp/totp.types.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)