Skip to content

Commit 9d06091

Browse files
Merge pull request #107 from contentstack/staging
DX | 29-09-2024 | Release
2 parents 9b32ec2 + 4d72626 commit 9d06091

27 files changed

+4703
-11
lines changed

.talismanrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ fileignoreconfig:
77
checksum: 854eb83dcacd62d3bf233c82e5cfd0c69dd20478fa0e7c6af9028f6c6386749d
88
- filename: Contentstack.Management.Core/Attributes/CSMJsonConverterAttribute.cs
99
checksum: 774bc2a4cf7f62fb890ba39ba1319769f0ff4e13d94781d394fcac2adf14381e
10+
- filename: Contentstack.Management.Core.Unit.Tests/OAuth/OAuthOptionsTest.cs
11+
checksum: 3d1ed19a9c7d311d9662632f48169d3a9013f605674cbd18b9f45039b0f83ff6
12+
- filename: Contentstack.Management.Core/Models/OAuthOptions.cs
13+
checksum: c328bfd5241e11e6e9d630527ba9084fb5b361abac48e8af2f96379dd6357c6c
1014
version: ""

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
# Changelog
2+
## [v0.4.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.4.0)
3+
- Feat
4+
- **MFA Support**: Added Multi-Factor Authentication (MFA) support for login operations
5+
- Added `mfaSecret` parameter to `Login` and `LoginAsync` methods for TOTP generation
6+
- Automatic TOTP token generation from Base32-encoded MFA secrets using Otp.NET library
7+
- Comprehensive test coverage for MFA functionality including unit and integration tests
8+
- Supports both explicit token and MFA secret-based authentication flows
9+
- Added Support for OAuth
10+
- Added Comprehensive test coverage for OAuth Functionality in Unit Test cases.
11+
- Supports both Login with and without OAuth Flows
12+
213
## [v0.3.2](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.3.2)
314
- Fix
415
- Added Test cases for the Release

Contentstack.Management.Core.Tests/IntegrationTest/Contentstack001_LoginTest.cs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,123 @@ public void Test007_Should_Return_Loggedin_User_With_Organizations_detail()
175175
Assert.Fail(e.Message);
176176
}
177177
}
178+
179+
[TestMethod]
180+
[DoNotParallelize]
181+
public void Test008_Should_Fail_Login_With_Invalid_MfaSecret()
182+
{
183+
ContentstackClient client = new ContentstackClient();
184+
NetworkCredential credentials = new NetworkCredential("test_user", "test_password");
185+
string invalidMfaSecret = "INVALID_BASE32_SECRET!@#";
186+
187+
try
188+
{
189+
ContentstackResponse contentstackResponse = client.Login(credentials, null, invalidMfaSecret);
190+
Assert.Fail("Expected exception for invalid MFA secret");
191+
}
192+
catch (ArgumentException)
193+
{
194+
// Expected exception for invalid Base32 encoding
195+
Assert.IsTrue(true);
196+
}
197+
catch (Exception e)
198+
{
199+
Assert.Fail($"Unexpected exception type: {e.GetType().Name} - {e.Message}");
200+
}
201+
}
202+
203+
[TestMethod]
204+
[DoNotParallelize]
205+
public void Test009_Should_Generate_TOTP_Token_With_Valid_MfaSecret()
206+
{
207+
ContentstackClient client = new ContentstackClient();
208+
NetworkCredential credentials = new NetworkCredential("test_user", "test_password");
209+
string validMfaSecret = "JBSWY3DPEHPK3PXP"; // Valid Base32 test secret
210+
211+
try
212+
{
213+
// This should fail due to invalid credentials, but should succeed in generating TOTP
214+
ContentstackResponse contentstackResponse = client.Login(credentials, null, validMfaSecret);
215+
}
216+
catch (ContentstackErrorException errorException)
217+
{
218+
// Expected to fail due to invalid credentials, but we verify it processed the MFA secret
219+
// The error should be about credentials, not about MFA secret format
220+
Assert.AreEqual(HttpStatusCode.UnprocessableEntity, errorException.StatusCode);
221+
Assert.IsTrue(errorException.Message.Contains("email or password") ||
222+
errorException.Message.Contains("credentials") ||
223+
errorException.Message.Contains("authentication"));
224+
}
225+
catch (ArgumentException)
226+
{
227+
Assert.Fail("Should not throw ArgumentException for valid MFA secret");
228+
}
229+
catch (Exception e)
230+
{
231+
Assert.Fail($"Unexpected exception type: {e.GetType().Name} - {e.Message}");
232+
}
233+
}
234+
235+
[TestMethod]
236+
[DoNotParallelize]
237+
public async System.Threading.Tasks.Task Test010_Should_Generate_TOTP_Token_With_Valid_MfaSecret_Async()
238+
{
239+
ContentstackClient client = new ContentstackClient();
240+
NetworkCredential credentials = new NetworkCredential("test_user", "test_password");
241+
string validMfaSecret = "JBSWY3DPEHPK3PXP"; // Valid Base32 test secret
242+
243+
try
244+
{
245+
// This should fail due to invalid credentials, but should succeed in generating TOTP
246+
ContentstackResponse contentstackResponse = await client.LoginAsync(credentials, null, validMfaSecret);
247+
}
248+
catch (ContentstackErrorException errorException)
249+
{
250+
// Expected to fail due to invalid credentials, but we verify it processed the MFA secret
251+
// The error should be about credentials, not about MFA secret format
252+
Assert.AreEqual(HttpStatusCode.UnprocessableEntity, errorException.StatusCode);
253+
Assert.IsTrue(errorException.Message.Contains("email or password") ||
254+
errorException.Message.Contains("credentials") ||
255+
errorException.Message.Contains("authentication"));
256+
}
257+
catch (ArgumentException)
258+
{
259+
Assert.Fail("Should not throw ArgumentException for valid MFA secret");
260+
}
261+
catch (Exception e)
262+
{
263+
Assert.Fail($"Unexpected exception type: {e.GetType().Name} - {e.Message}");
264+
}
265+
}
266+
267+
[TestMethod]
268+
[DoNotParallelize]
269+
public void Test011_Should_Prefer_Explicit_Token_Over_MfaSecret()
270+
{
271+
ContentstackClient client = new ContentstackClient();
272+
NetworkCredential credentials = new NetworkCredential("test_user", "test_password");
273+
string validMfaSecret = "JBSWY3DPEHPK3PXP";
274+
string explicitToken = "123456";
275+
276+
try
277+
{
278+
// This should fail due to invalid credentials, but should use explicit token
279+
ContentstackResponse contentstackResponse = client.Login(credentials, explicitToken, validMfaSecret);
280+
}
281+
catch (ContentstackErrorException errorException)
282+
{
283+
// Expected to fail due to invalid credentials
284+
// The important thing is that it didn't throw an exception about MFA secret processing
285+
Assert.AreEqual(HttpStatusCode.UnprocessableEntity, errorException.StatusCode);
286+
}
287+
catch (ArgumentException)
288+
{
289+
Assert.Fail("Should not throw ArgumentException when explicit token is provided");
290+
}
291+
catch (Exception e)
292+
{
293+
Assert.Fail($"Unexpected exception type: {e.GetType().Name} - {e.Message}");
294+
}
295+
}
178296
}
179297
}

Contentstack.Management.Core.Unit.Tests/Core/Services/User/LoginServiceTest.cs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,107 @@ public void Should_Allow_Credentials_With_Token()
5050
Assert.AreEqual("{\"user\":{\"email\":\"name\",\"password\":\"password\",\"tfa_token\":\"token\"}}", Encoding.Default.GetString(loginService.ByteContent));
5151
}
5252

53+
[TestMethod]
54+
public void Should_Allow_Credentials_With_MfaSecret()
55+
{
56+
57+
string testMfaSecret = "JBSWY3DPEHPK3PXP"; // Base32 encoded "Hello!"
58+
var loginService = new LoginService(serializer, credentials, null, testMfaSecret);
59+
loginService.ContentBody();
60+
61+
Assert.IsNotNull(loginService);
62+
var contentString = Encoding.Default.GetString(loginService.ByteContent);
63+
64+
Assert.IsTrue(contentString.Contains("\"email\":\"name\""));
65+
Assert.IsTrue(contentString.Contains("\"password\":\"password\""));
66+
Assert.IsTrue(contentString.Contains("\"tfa_token\":"));
67+
68+
// Verify the tfa_token is not null or empty in the JSON
69+
Assert.IsFalse(contentString.Contains("\"tfa_token\":null"));
70+
Assert.IsFalse(contentString.Contains("\"tfa_token\":\"\""));
71+
}
72+
73+
[TestMethod]
74+
public void Should_Generate_TOTP_Token_When_MfaSecret_Provided()
75+
{
76+
string testMfaSecret = "JBSWY3DPEHPK3PXP"; // Base32 encoded "Hello!"
77+
var loginService1 = new LoginService(serializer, credentials, null, testMfaSecret);
78+
var loginService2 = new LoginService(serializer, credentials, null, testMfaSecret);
79+
80+
loginService1.ContentBody();
81+
loginService2.ContentBody();
82+
83+
var content1 = Encoding.Default.GetString(loginService1.ByteContent);
84+
var content2 = Encoding.Default.GetString(loginService2.ByteContent);
85+
86+
// Both should contain tfa_token
87+
Assert.IsTrue(content1.Contains("\"tfa_token\":"));
88+
Assert.IsTrue(content2.Contains("\"tfa_token\":"));
89+
90+
// Extract the tokens for comparison (tokens should be 6 digits)
91+
var token1Match = System.Text.RegularExpressions.Regex.Match(content1, "\"tfa_token\":\"(\\d{6})\"");
92+
var token2Match = System.Text.RegularExpressions.Regex.Match(content2, "\"tfa_token\":\"(\\d{6})\"");
93+
94+
Assert.IsTrue(token1Match.Success);
95+
Assert.IsTrue(token2Match.Success);
96+
97+
// Tokens should be valid 6-digit numbers
98+
Assert.AreEqual(6, token1Match.Groups[1].Value.Length);
99+
Assert.AreEqual(6, token2Match.Groups[1].Value.Length);
100+
}
101+
102+
[TestMethod]
103+
public void Should_Prefer_Explicit_Token_Over_MfaSecret()
104+
{
105+
string testMfaSecret = "JBSWY3DPEHPK3PXP";
106+
// file deepcode ignore NoHardcodedCredentials/test: random test token
107+
string explicitToken = "123456";
108+
109+
var loginService = new LoginService(serializer, credentials, explicitToken, testMfaSecret);
110+
loginService.ContentBody();
111+
112+
var contentString = Encoding.Default.GetString(loginService.ByteContent);
113+
114+
// Should use the explicit token, not generate one from MFA secret
115+
Assert.IsTrue(contentString.Contains("\"tfa_token\":\"123456\""));
116+
}
117+
118+
[TestMethod]
119+
[ExpectedException(typeof(ArgumentException))]
120+
public void Should_Throw_Exception_For_Invalid_Base32_MfaSecret()
121+
{
122+
// Invalid Base32 secret (contains invalid characters)
123+
string invalidMfaSecret = "INVALID_BASE32_123!@#";
124+
125+
var loginService = new LoginService(serializer, credentials, null, invalidMfaSecret);
126+
}
127+
128+
[TestMethod]
129+
public void Should_Not_Generate_Token_When_MfaSecret_Is_Empty()
130+
{
131+
var loginService = new LoginService(serializer, credentials, null, "");
132+
loginService.ContentBody();
133+
134+
var contentString = Encoding.Default.GetString(loginService.ByteContent);
135+
136+
// Should not contain tfa_token when MFA secret is empty
137+
Assert.IsFalse(contentString.Contains("\"tfa_token\":"));
138+
Assert.AreEqual("{\"user\":{\"email\":\"name\",\"password\":\"password\"}}", contentString);
139+
}
140+
141+
[TestMethod]
142+
public void Should_Not_Generate_Token_When_MfaSecret_Is_Null()
143+
{
144+
var loginService = new LoginService(serializer, credentials, null, null);
145+
loginService.ContentBody();
146+
147+
var contentString = Encoding.Default.GetString(loginService.ByteContent);
148+
149+
// Should not contain tfa_token when MFA secret is null
150+
Assert.IsFalse(contentString.Contains("\"tfa_token\":"));
151+
Assert.AreEqual("{\"user\":{\"email\":\"name\",\"password\":\"password\"}}", contentString);
152+
}
153+
53154
[TestMethod]
54155
public void Should_Override_Authtoken_To_ContentstackOptions_On_Success()
55156
{

0 commit comments

Comments
 (0)