Skip to content

Commit 0a902b3

Browse files
committed
Oauth Support in Dotnet Management SDK
1 parent 6296468 commit 0a902b3

7 files changed

Lines changed: 233 additions & 375 deletions

File tree

Contentstack.Management.Core/ContentstackClient.cs

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public class ContentstackClient : IContentstackClient
4040

4141
// OAuth token storage
4242
private readonly Dictionary<string, OAuthTokens> _oauthTokens = new Dictionary<string, OAuthTokens>();
43+
44+
private bool _isRefreshingToken = false;
4345
#endregion
4446

4547

@@ -228,20 +230,26 @@ internal ContentstackResponse InvokeSync<TRequest>(TRequest request, bool addAcc
228230
return (ContentstackResponse)ContentstackPipeline.InvokeSync(context, addAcceptMediaHeader, apiVersion).httpResponse;
229231
}
230232

231-
internal Task<TResponse> InvokeAsync<TRequest, TResponse>(TRequest request, bool addAcceptMediaHeader = false, string apiVersion = null)
233+
internal async Task<TResponse> InvokeAsync<TRequest, TResponse>(TRequest request, bool addAcceptMediaHeader = false, string apiVersion = null)
232234
where TRequest : IContentstackService
233235
where TResponse : ContentstackResponse
234236
{
235237
ThrowIfDisposed();
236238

239+
// Check and refresh OAuth tokens if needed before making API calls
240+
if (contentstackOptions.IsOAuthToken && !string.IsNullOrEmpty(contentstackOptions.Authtoken))
241+
{
242+
await EnsureOAuthTokenIsValidAsync();
243+
}
244+
237245
ExecutionContext context = new ExecutionContext(
238246
new RequestContext()
239247
{
240248
config = contentstackOptions,
241249
service = request
242250
},
243251
new ResponseContext());
244-
return ContentstackPipeline.InvokeAsync<TResponse>(context, addAcceptMediaHeader, apiVersion);
252+
return await ContentstackPipeline.InvokeAsync<TResponse>(context, addAcceptMediaHeader, apiVersion);
245253
}
246254

247255
#region Dispose methods
@@ -502,6 +510,8 @@ internal void SetOAuthTokens(OAuthTokens tokens)
502510

503511
// Store the access token in the client options for use in HTTP requests
504512
// This will be used by the HTTP pipeline to inject the Bearer token
513+
// Note: We need both IsOAuthToken=true AND Authtoken=AccessToken because
514+
// the HTTP pipeline only has access to ContentstackClientOptions, not the full client
505515
contentstackOptions.Authtoken = tokens.AccessToken;
506516
contentstackOptions.IsOAuthToken = true;
507517
}
@@ -665,5 +675,74 @@ public Task<ContentstackResponse> GetUserAsync(ParameterCollection collection =
665675

666676
return InvokeAsync<GetLoggedInUserService, ContentstackResponse>(getUser);
667677
}
678+
679+
/// <summary>
680+
/// Ensures that the current OAuth token is valid and refreshes it if needed.
681+
/// This method is called before each API request to automatically handle token refresh.
682+
/// </summary>
683+
private async Task EnsureOAuthTokenIsValidAsync()
684+
{
685+
// Prevent recursive calls
686+
if (_isRefreshingToken)
687+
{
688+
return;
689+
}
690+
691+
try
692+
{
693+
// Find the OAuth tokens that match the current access token
694+
foreach (var kvp in _oauthTokens)
695+
{
696+
var clientId = kvp.Key;
697+
var tokens = kvp.Value;
698+
699+
if (tokens?.AccessToken == contentstackOptions.Authtoken && tokens.NeedsRefresh)
700+
{
701+
// Set flag to prevent recursive calls
702+
_isRefreshingToken = true;
703+
704+
try
705+
{
706+
// Get the OAuth handler for this client
707+
var oauthHandler = OAuth(new Models.OAuthOptions
708+
{
709+
ClientId = clientId,
710+
AppId = tokens.AppId
711+
});
712+
713+
// Refresh the tokens
714+
var refreshedTokens = await oauthHandler.RefreshTokenAsync(tokens.RefreshToken);
715+
716+
if (refreshedTokens != null)
717+
{
718+
// Update the stored tokens
719+
StoreOAuthTokens(clientId, refreshedTokens);
720+
721+
// Update the client's current authentication
722+
contentstackOptions.Authtoken = refreshedTokens.AccessToken;
723+
contentstackOptions.IsOAuthToken = true; // Ensure OAuth flag is maintained
724+
}
725+
}
726+
catch (Exception ex)
727+
{
728+
// Wrap any exception in OAuth exception with context
729+
throw new Exceptions.OAuthException(
730+
$"OAuth token refresh failed for client '{clientId}': {ex.Message}", ex);
731+
}
732+
finally
733+
{
734+
_isRefreshingToken = false;
735+
}
736+
}
737+
}
738+
}
739+
catch (Exception ex)
740+
{
741+
// Wrap any exception in OAuth exception with context
742+
throw new Exceptions.OAuthException(
743+
$"OAuth token validation failed: {ex.Message}", ex);
744+
}
745+
}
668746
}
669747
}
748+

Contentstack.Management.Core/Exceptions/OAuthException.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class OAuthException : ContentstackException
1010
/// <summary>
1111
/// Initializes a new instance of the OAuthException class.
1212
/// </summary>
13-
public OAuthException() : base("An OAuth error occurred.")
13+
public OAuthException() : base("OAuth operation failed.")
1414
{
1515
}
1616

@@ -40,7 +40,7 @@ public class OAuthConfigurationException : OAuthException
4040
/// <summary>
4141
/// Initializes a new instance of the OAuthConfigurationException class.
4242
/// </summary>
43-
public OAuthConfigurationException() : base("OAuth configuration error occurred.")
43+
public OAuthConfigurationException() : base("OAuth configuration is invalid.")
4444
{
4545
}
4646

@@ -70,7 +70,7 @@ public class OAuthTokenException : OAuthException
7070
/// <summary>
7171
/// Initializes a new instance of the OAuthTokenException class.
7272
/// </summary>
73-
public OAuthTokenException() : base("OAuth token error occurred.")
73+
public OAuthTokenException() : base("OAuth token operation failed.")
7474
{
7575
}
7676

@@ -100,7 +100,7 @@ public class OAuthAuthorizationException : OAuthException
100100
/// <summary>
101101
/// Initializes a new instance of the OAuthAuthorizationException class.
102102
/// </summary>
103-
public OAuthAuthorizationException() : base("OAuth authorization error occurred.")
103+
public OAuthAuthorizationException() : base("OAuth authorization failed.")
104104
{
105105
}
106106

@@ -130,7 +130,7 @@ public class OAuthTokenRefreshException : OAuthTokenException
130130
/// <summary>
131131
/// Initializes a new instance of the OAuthTokenRefreshException class.
132132
/// </summary>
133-
public OAuthTokenRefreshException() : base("OAuth token refresh error occurred.")
133+
public OAuthTokenRefreshException() : base("OAuth token refresh failed.")
134134
{
135135
}
136136

Contentstack.Management.Core/Models/OAuthAppAuthorizationResponse.cs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ namespace Contentstack.Management.Core.Models
88
/// </summary>
99
public class OAuthAppAuthorizationResponse
1010
{
11-
/// <summary>
12-
/// Array of OAuth app authorization data.
13-
/// </summary>
11+
1412
[JsonProperty("data")]
1513
public OAuthAppAuthorizationData[] Data { get; set; }
1614
}
@@ -20,27 +18,19 @@ public class OAuthAppAuthorizationResponse
2018
/// </summary>
2119
public class OAuthAppAuthorizationData
2220
{
23-
/// <summary>
24-
/// The authorization UID.
25-
/// </summary>
21+
2622
[JsonProperty("authorization_uid")]
2723
public string AuthorizationUid { get; set; }
2824

29-
/// <summary>
30-
/// The user information.
31-
/// </summary>
25+
3226
[JsonProperty("user")]
3327
public OAuthUser User { get; set; }
3428
}
3529

36-
/// <summary>
37-
/// Represents OAuth user information.
38-
/// </summary>
30+
3931
public class OAuthUser
4032
{
41-
/// <summary>
42-
/// The user UID.
43-
/// </summary>
33+
4434
[JsonProperty("uid")]
4535
public string Uid { get; set; }
4636
}

Contentstack.Management.Core/Models/OAuthOptions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ namespace Contentstack.Management.Core.Models
88
public class OAuthOptions
99
{
1010
/// <summary>
11-
/// The OAuth application ID. Defaults to the Contentstack demo app ID.
11+
/// The OAuth application ID. Defaults to the Contentstack app ID.
1212
/// </summary>
1313
public string AppId { get; set; } = "6400aa06db64de001a31c8a9";
1414

1515
/// <summary>
16-
/// The OAuth client ID. Defaults to the Contentstack demo client ID.
16+
/// The OAuth client ID. Defaults to the Contentstack client ID.
1717
/// </summary>
1818
public string ClientId { get; set; } = "Ie0FEfTzlfAHL4xM";
1919

Contentstack.Management.Core/Models/OAuthResponse.cs

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,25 @@ namespace Contentstack.Management.Core.Models
88
/// </summary>
99
public class OAuthResponse
1010
{
11-
/// <summary>
12-
/// The access token used for API authentication.
13-
/// </summary>
11+
1412
[JsonProperty("access_token")]
1513
public string AccessToken { get; set; }
1614

17-
/// <summary>
18-
/// The refresh token used to obtain new access tokens.
19-
/// </summary>
15+
2016
[JsonProperty("refresh_token")]
2117
public string RefreshToken { get; set; }
2218

23-
/// <summary>
24-
/// The number of seconds until the access token expires.
25-
/// </summary>
19+
2620
[JsonProperty("expires_in")]
2721
public int ExpiresIn { get; set; }
2822

29-
/// <summary>
30-
/// The organization UID associated with the OAuth tokens.
31-
/// </summary>
23+
3224
[JsonProperty("organization_uid")]
3325
public string OrganizationUid { get; set; }
3426

35-
/// <summary>
36-
/// The user UID associated with the OAuth tokens.
37-
/// </summary>
27+
3828
[JsonProperty("user_uid")]
3929
public string UserUid { get; set; }
40-
41-
/// <summary>
42-
/// The type of authorization (e.g., "oauth").
43-
/// </summary>
44-
[JsonProperty("authorization_type")]
45-
public string AuthorizationType { get; set; }
46-
47-
/// <summary>
48-
/// The stack API key associated with the OAuth tokens.
49-
/// </summary>
50-
[JsonProperty("stack_api_key")]
51-
public string StackApiKey { get; set; }
5230
}
5331
}
5432

Contentstack.Management.Core/Models/OAuthTokens.cs

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,29 @@ namespace Contentstack.Management.Core.Models
44
{
55
/// <summary>
66
/// Represents OAuth tokens stored in memory for cross-SDK access.
7-
/// This class enables sharing OAuth tokens between the Management SDK and other SDKs like Model Generator.
7+
/// This class enables sharing OAuth tokens between the Management SDK and other SDKs
88
/// </summary>
99
public class OAuthTokens
1010
{
11-
/// <summary>
12-
/// Gets or sets the access token used for API authentication.
13-
/// </summary>
11+
1412
public string AccessToken { get; set; }
1513

16-
/// <summary>
17-
/// Gets or sets the refresh token used to obtain new access tokens.
18-
/// </summary>
1914
public string RefreshToken { get; set; }
2015

21-
/// <summary>
22-
/// Gets or sets the date and time when the access token expires.
23-
/// </summary>
2416
public DateTime ExpiresAt { get; set; }
2517

26-
/// <summary>
27-
/// Gets or sets the organization UID associated with the OAuth tokens.
28-
/// </summary>
2918
public string OrganizationUid { get; set; }
3019

31-
/// <summary>
32-
/// Gets or sets the user UID associated with the OAuth tokens.
33-
/// </summary>
3420
public string UserUid { get; set; }
3521

36-
/// <summary>
37-
/// Gets or sets the OAuth client ID associated with these tokens.
38-
/// </summary>
3922
public string ClientId { get; set; }
4023

41-
/// <summary>
42-
/// Gets a value indicating whether the access token has expired.
43-
/// </summary>
24+
public string AppId { get; set; }
25+
4426
public bool IsExpired => DateTime.UtcNow >= ExpiresAt;
4527

46-
/// <summary>
47-
/// Gets a value indicating whether the access token needs to be refreshed.
48-
/// Tokens are considered to need refresh if they expire within 5 minutes or are already expired.
49-
/// </summary>
5028
public bool NeedsRefresh => DateTime.UtcNow >= ExpiresAt.AddMinutes(-5) || IsExpired;
5129

52-
/// <summary>
53-
/// Gets a value indicating whether the OAuth tokens are valid for use.
54-
/// Tokens are valid if they have an access token and are not expired.
55-
/// </summary>
5630
public bool IsValid => !string.IsNullOrEmpty(AccessToken) && !IsExpired;
5731
}
5832
}

0 commit comments

Comments
 (0)