55using System . Net ;
66using System . Net . Http ;
77using System . Reflection ;
8+ using System . Threading . Tasks ;
9+ using Contentstack . Management . Core . Exceptions ;
810using Contentstack . Management . Core . Tests . Helpers ;
911using Contentstack . Management . Core . Tests . Model ;
1012using Microsoft . Extensions . Configuration ;
@@ -35,12 +37,195 @@ private static readonly Lazy<IConfigurationRoot>
3537 return Config . GetSection ( "Contentstack:Organization" ) . Get < OrganizationModel > ( ) ;
3638 } ) ;
3739
40+ private static readonly Lazy < string > mfaSecret =
41+ new Lazy < string > ( ( ) =>
42+ {
43+ return Config . GetSection ( "Contentstack:MfaSecret" ) . Value ;
44+ } ) ;
45+
3846 public static IConfigurationRoot Config { get { return config . Value ; } }
3947 public static NetworkCredential Credential { get { return credential . Value ; } }
4048 public static OrganizationModel Organization { get { return organization . Value ; } }
49+ public static string MfaSecret { get { return mfaSecret . Value ; } }
4150
4251 public static StackModel Stack { get ; set ; }
4352
53+ // TOTP token tracking to prevent reuse
54+ private static readonly HashSet < string > _usedTotpTokens = new HashSet < string > ( ) ;
55+ private static DateTime _lastTotpGeneration = DateTime . MinValue ;
56+ private static readonly object _totpLock = new object ( ) ;
57+
58+ /// <summary>
59+ /// Checks if the exception indicates TOTP token reuse
60+ /// </summary>
61+ public static bool IsTotpReuse ( Exception exception )
62+ {
63+ if ( exception is ContentstackErrorException csException )
64+ {
65+ return csException . ErrorMessage ? . Contains ( "Totp has already been Used" ) == true ;
66+ }
67+ return false ;
68+ }
69+
70+ /// <summary>
71+ /// Checks if the exception indicates an account lockout
72+ /// </summary>
73+ public static bool IsAccountLockout ( Exception exception )
74+ {
75+ if ( exception is ContentstackErrorException csException )
76+ {
77+ return csException . ErrorCode == 104 &&
78+ ( csException . ErrorMessage ? . Contains ( "locked" ) == true ||
79+ csException . ErrorMessage ? . Contains ( "temporarily" ) == true ) ;
80+ }
81+ return false ;
82+ }
83+
84+ /// <summary>
85+ /// Ensures sufficient time has passed for fresh TOTP token generation
86+ /// </summary>
87+ public static void EnsureFreshTotpWindow ( )
88+ {
89+ lock ( _totpLock )
90+ {
91+ var timeSinceLastTotp = DateTime . UtcNow - _lastTotpGeneration ;
92+ if ( timeSinceLastTotp . TotalSeconds < 35 )
93+ {
94+ int sleepMs = ( int ) ( 35000 - timeSinceLastTotp . TotalMilliseconds ) ;
95+ System . Threading . Thread . Sleep ( sleepMs ) ;
96+ }
97+
98+ // Clean up old tokens (older than 2 minutes)
99+ var cutoff = DateTime . UtcNow . AddMinutes ( - 2 ) ;
100+ if ( _lastTotpGeneration < cutoff )
101+ {
102+ _usedTotpTokens . Clear ( ) ;
103+ }
104+
105+ _lastTotpGeneration = DateTime . UtcNow ;
106+ }
107+ }
108+
109+ /// <summary>
110+ /// Executes login with retry logic for account lockouts
111+ /// </summary>
112+ public static ContentstackResponse LoginWithRetry ( ContentstackClient client , int maxRetries = 3 , int baseDelayMs = 5000 )
113+ {
114+ for ( int attempt = 0 ; attempt <= maxRetries ; attempt ++ )
115+ {
116+ try
117+ {
118+ return client . Login ( Credential , null , MfaSecret ) ;
119+ }
120+ catch ( Exception ex ) when ( IsAccountLockout ( ex ) && attempt < maxRetries )
121+ {
122+ int delay = baseDelayMs * ( int ) Math . Pow ( 2 , attempt ) ; // Exponential backoff
123+ System . Threading . Thread . Sleep ( delay ) ;
124+ }
125+ }
126+ // Final attempt without catching lockout
127+ return client . Login ( Credential , null , MfaSecret ) ;
128+ }
129+
130+ /// <summary>
131+ /// Executes async login with retry logic for account lockouts
132+ /// </summary>
133+ public static async Task < ContentstackResponse > LoginWithRetryAsync ( ContentstackClient client , int maxRetries = 3 , int baseDelayMs = 5000 )
134+ {
135+ for ( int attempt = 0 ; attempt <= maxRetries ; attempt ++ )
136+ {
137+ try
138+ {
139+ return await client . LoginAsync ( Credential , null , MfaSecret ) ;
140+ }
141+ catch ( Exception ex ) when ( IsAccountLockout ( ex ) && attempt < maxRetries )
142+ {
143+ int delay = baseDelayMs * ( int ) Math . Pow ( 2 , attempt ) ; // Exponential backoff
144+ await Task . Delay ( delay ) ;
145+ }
146+ }
147+ // Final attempt without catching lockout
148+ return await client . LoginAsync ( Credential , null , MfaSecret ) ;
149+ }
150+
151+ /// <summary>
152+ /// Executes login with TOTP-aware retry logic for token reuse and account lockouts
153+ /// </summary>
154+ public static ContentstackResponse LoginWithTotpRetry ( ContentstackClient client , int maxRetries = 3 )
155+ {
156+ for ( int attempt = 0 ; attempt <= maxRetries ; attempt ++ )
157+ {
158+ try
159+ {
160+ // Ensure fresh TOTP window before each attempt
161+ EnsureFreshTotpWindow ( ) ;
162+ return client . Login ( Credential , null , MfaSecret ) ;
163+ }
164+ catch ( Exception ex ) when ( attempt < maxRetries )
165+ {
166+ if ( IsTotpReuse ( ex ) )
167+ {
168+ // Wait for fresh TOTP window (35+ seconds)
169+ System . Threading . Thread . Sleep ( 35000 ) ;
170+ }
171+ else if ( IsAccountLockout ( ex ) )
172+ {
173+ // Exponential backoff for account lockout
174+ int delay = 5000 * ( int ) Math . Pow ( 2 , attempt ) ;
175+ System . Threading . Thread . Sleep ( delay ) ;
176+ }
177+ else
178+ {
179+ // For other errors, short delay before retry
180+ System . Threading . Thread . Sleep ( 1000 ) ;
181+ }
182+ }
183+ }
184+
185+ // Final attempt without catching errors
186+ EnsureFreshTotpWindow ( ) ;
187+ return client . Login ( Credential , null , MfaSecret ) ;
188+ }
189+
190+ /// <summary>
191+ /// Executes async login with TOTP-aware retry logic for token reuse and account lockouts
192+ /// </summary>
193+ public static async Task < ContentstackResponse > LoginWithTotpRetryAsync ( ContentstackClient client , int maxRetries = 3 )
194+ {
195+ for ( int attempt = 0 ; attempt <= maxRetries ; attempt ++ )
196+ {
197+ try
198+ {
199+ // Ensure fresh TOTP window before each attempt
200+ EnsureFreshTotpWindow ( ) ;
201+ return await client . LoginAsync ( Credential , null , MfaSecret ) ;
202+ }
203+ catch ( Exception ex ) when ( attempt < maxRetries )
204+ {
205+ if ( IsTotpReuse ( ex ) )
206+ {
207+ // Wait for fresh TOTP window (35+ seconds)
208+ await Task . Delay ( 35000 ) ;
209+ }
210+ else if ( IsAccountLockout ( ex ) )
211+ {
212+ // Exponential backoff for account lockout
213+ int delay = 5000 * ( int ) Math . Pow ( 2 , attempt ) ;
214+ await Task . Delay ( delay ) ;
215+ }
216+ else
217+ {
218+ // For other errors, short delay before retry
219+ await Task . Delay ( 1000 ) ;
220+ }
221+ }
222+ }
223+
224+ // Final attempt without catching errors
225+ EnsureFreshTotpWindow ( ) ;
226+ return await client . LoginAsync ( Credential , null , MfaSecret ) ;
227+ }
228+
44229 /// <summary>
45230 /// Creates a new ContentstackClient, logs in via the Login API (never from config),
46231 /// and returns the authenticated client. Callers are responsible for calling Logout()
@@ -53,7 +238,7 @@ public static ContentstackClient CreateAuthenticatedClient()
53238 var handler = new LoggingHttpHandler ( ) ;
54239 var httpClient = new HttpClient ( handler ) ;
55240 var client = new ContentstackClient ( httpClient , options ) ;
56- client . Login ( Credential ) ;
241+ LoginWithTotpRetry ( client ) ;
57242 return client ;
58243 }
59244
0 commit comments