Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Text.Json;
using GitCredentialManager;
using Xunit;

namespace Atlassian.Bitbucket.Tests
Expand All @@ -17,11 +18,7 @@ public void BitbucketTokenEndpointResponseJson_Deserialize_Uses_Scopes()

var json = $"{{\"access_token\": \"{accessToken}\", \"token_type\": \"{tokenType}\", \"expires_in\": {expiresIn}, \"scopes\": \"{scopesString}\", \"scope\": \"{scopeString}\"}}";

var result = JsonSerializer.Deserialize<BitbucketTokenEndpointResponseJson>(json,
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
var result = JsonSerializer.Deserialize<BitbucketTokenEndpointResponseJson>(json, JsonHelper.CaseInsensitiveOptions);

Assert.Equal(accessToken, result.AccessToken);
Assert.Equal(tokenType, result.TokenType);
Expand Down
6 changes: 2 additions & 4 deletions src/shared/Atlassian.Bitbucket.Tests/Cloud/UserInfoTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Atlassian.Bitbucket.Cloud;
using GitCredentialManager;
using Xunit;

namespace Atlassian.Bitbucket.Tests.Cloud
Expand All @@ -29,10 +30,7 @@ public void Deserialize_UserInfo()

var json = $"{{\"uuid\": \"{uuid}\", \"has_2fa_enabled\": null, \"username\": \"{userName}\", \"account_id\": \"{accountId}\"}}";

var result = JsonSerializer.Deserialize<UserInfo>(json, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
});
var result = JsonSerializer.Deserialize<UserInfo>(json, JsonHelper.CaseInsensitiveOptions);

Assert.Equal(userName, result.UserName);
}
Expand Down
39 changes: 12 additions & 27 deletions src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,29 @@ public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2

private static string GetClientId(ISettings settings)
{
// Check for developer override value
if (settings.TryGetSetting(
return settings.GetOAuthConfigValue(
CloudConstants.EnvironmentVariables.OAuthClientId,
Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthClientId,
out string clientId))
{
return clientId;
}

return CloudConstants.OAuth2ClientId;
Constants.GitConfiguration.Credential.SectionName,
CloudConstants.GitConfiguration.Credential.OAuthClientId,
CloudConstants.OAuth2ClientId);
}

private static Uri GetRedirectUri(ISettings settings)
{
// Check for developer override value
if (settings.TryGetSetting(
return settings.GetOAuthConfigUri(
CloudConstants.EnvironmentVariables.OAuthRedirectUri,
Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthRedirectUri,
out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri))
{
return redirectUri;
}

return CloudConstants.OAuth2RedirectUri;
Constants.GitConfiguration.Credential.SectionName,
CloudConstants.GitConfiguration.Credential.OAuthRedirectUri,
CloudConstants.OAuth2RedirectUri);
}

private static string GetClientSecret(ISettings settings)
{
// Check for developer override value
if (settings.TryGetSetting(
return settings.GetOAuthConfigValue(
CloudConstants.EnvironmentVariables.OAuthClientSecret,
Constants.GitConfiguration.Credential.SectionName, CloudConstants.GitConfiguration.Credential.OAuthClientSecret,
out string clientSecret))
{
return clientSecret;
}

return CloudConstants.OAuth2ClientSecret;
Constants.GitConfiguration.Credential.SectionName,
CloudConstants.GitConfiguration.Credential.OAuthClientSecret,
CloudConstants.OAuth2ClientSecret);
}

private static OAuth2ServerEndpoints GetEndpoints()
Expand Down
6 changes: 1 addition & 5 deletions src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ public async Task<RestApiResult<IUserInfo>> GetUserInformationAsync(string userN

if (response.IsSuccessStatusCode)
{
var obj = JsonSerializer.Deserialize<UserInfo>(json,
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
});
var obj = JsonSerializer.Deserialize<UserInfo>(json, JsonHelper.CaseInsensitiveOptions);

return new RestApiResult<IUserInfo>(response.StatusCode, obj);
}
Expand Down
39 changes: 12 additions & 27 deletions src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,44 +26,29 @@ public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2

private static string GetClientId(ISettings settings)
{
// Check for developer override value
if (settings.TryGetSetting(
return settings.GetRequiredOAuthConfigValue(
DataCenterConstants.EnvironmentVariables.OAuthClientId,
Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientId,
out string clientId))
{
return clientId;
}

throw new ArgumentException("Bitbucket DC OAuth Client ID must be defined");
Constants.GitConfiguration.Credential.SectionName,
DataCenterConstants.GitConfiguration.Credential.OAuthClientId,
"Bitbucket DC OAuth Client ID must be defined");
}

private static Uri GetRedirectUri(ISettings settings)
{
// Check for developer override value
if (settings.TryGetSetting(
return settings.GetOAuthConfigUri(
DataCenterConstants.EnvironmentVariables.OAuthRedirectUri,
Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthRedirectUri,
out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri))
{
return redirectUri;
}

return DataCenterConstants.OAuth2RedirectUri;
Constants.GitConfiguration.Credential.SectionName,
DataCenterConstants.GitConfiguration.Credential.OAuthRedirectUri,
DataCenterConstants.OAuth2RedirectUri);
}

private static string GetClientSecret(ISettings settings)
{
// Check for developer override value
if (settings.TryGetSetting(
return settings.GetRequiredOAuthConfigValue(
DataCenterConstants.EnvironmentVariables.OAuthClientSecret,
Constants.GitConfiguration.Credential.SectionName, DataCenterConstants.GitConfiguration.Credential.OAuthClientSecret,
out string clientSecret))
{
return clientSecret;
}

throw new ArgumentException("Bitbucket DC OAuth Client Secret must be defined");
Constants.GitConfiguration.Credential.SectionName,
DataCenterConstants.GitConfiguration.Credential.OAuthClientSecret,
"Bitbucket DC OAuth Client Secret must be defined");
}

private static OAuth2ServerEndpoints GetEndpoints(ISettings settings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,7 @@ public async Task<List<AuthenticationMethod>> GetAuthenticationMethodsAsync()

if (response.IsSuccessStatusCode)
{
var loginOptions = JsonSerializer.Deserialize<LoginOptions>(json,
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
var loginOptions = JsonSerializer.Deserialize<LoginOptions>(json, JsonHelper.CaseInsensitiveIgnoreNullOptions);

if (loginOptions.Results.Any(r => "LOGIN_FORM".Equals(r.Type)))
{
Expand Down
7 changes: 2 additions & 5 deletions src/shared/Core.Tests/TokenEndpointResponseJsonTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Text.Json;
using GitCredentialManager;
using GitCredentialManager.Authentication.OAuth.Json;
using Xunit;

Expand All @@ -17,11 +18,7 @@ public void TokenEndpointResponseJson_Deserialize_Uses_Scope()
var scopeString = "a,b,c";
var json = $"{{\"access_token\": \"{accessToken}\", \"token_type\": \"{tokenType}\", \"expires_in\": {expiresIn}, \"scopes\": \"{scopesString}\", \"scope\": \"{scopeString}\"}}";

var result = JsonSerializer.Deserialize<TokenEndpointResponseJson>(json,
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
var result = JsonSerializer.Deserialize<TokenEndpointResponseJson>(json, JsonHelper.CaseInsensitiveOptions);

Assert.Equal(accessToken, result.AccessToken);
Assert.Equal(tokenType, result.TokenType);
Expand Down
5 changes: 1 addition & 4 deletions src/shared/Core/Authentication/OAuth/OAuth2Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,7 @@ public async Task<OAuth2TokenResult> GetTokenByDeviceCodeAsync(OAuth2DeviceCodeR
return result;
}

var error = JsonSerializer.Deserialize<ErrorResponseJson>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
var error = JsonSerializer.Deserialize<ErrorResponseJson>(json, JsonHelper.CaseInsensitiveOptions);

switch (error.Error)
{
Expand Down
84 changes: 84 additions & 0 deletions src/shared/Core/Authentication/OAuth/OAuth2SettingsHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;

namespace GitCredentialManager.Authentication.OAuth
{
/// <summary>
/// Helper class for retrieving OAuth2 configuration settings from environment variables or Git configuration.
/// </summary>
public static class OAuth2SettingsHelper
{
/// <summary>
/// Retrieves an OAuth configuration value from settings, with fallback to a default value.
/// </summary>
/// <param name="settings">The settings instance to query.</param>
/// <param name="environmentVariable">The environment variable name.</param>
/// <param name="configSection">The Git configuration section name.</param>
/// <param name="configProperty">The Git configuration property name.</param>
/// <param name="defaultValue">The default value to return if no setting is found.</param>
/// <returns>The configured value if found, otherwise the default value.</returns>
public static string GetOAuthConfigValue(
this ISettings settings,
string environmentVariable,
string configSection,
string configProperty,
string defaultValue)
{
if (settings.TryGetSetting(environmentVariable, configSection, configProperty, out string value))
{
return value;
}

return defaultValue;
}

/// <summary>
/// Retrieves a required OAuth configuration value from settings, throwing an exception if not found.
/// </summary>
/// <param name="settings">The settings instance to query.</param>
/// <param name="environmentVariable">The environment variable name.</param>
/// <param name="configSection">The Git configuration section name.</param>
/// <param name="configProperty">The Git configuration property name.</param>
/// <param name="errorMessage">The error message to include in the exception if the value is not found.</param>
/// <returns>The configured value.</returns>
/// <exception cref="ArgumentException">Thrown when the required value is not found.</exception>
public static string GetRequiredOAuthConfigValue(
this ISettings settings,
string environmentVariable,
string configSection,
string configProperty,
string errorMessage)
{
if (settings.TryGetSetting(environmentVariable, configSection, configProperty, out string value))
{
return value;
}

throw new ArgumentException(errorMessage);
}

/// <summary>
/// Retrieves an OAuth configuration URI from settings, with fallback to a default value.
/// </summary>
/// <param name="settings">The settings instance to query.</param>
/// <param name="environmentVariable">The environment variable name.</param>
/// <param name="configSection">The Git configuration section name.</param>
/// <param name="configProperty">The Git configuration property name.</param>
/// <param name="defaultValue">The default URI to return if no setting is found.</param>
/// <returns>The configured URI if found and valid, otherwise the default URI.</returns>
public static Uri GetOAuthConfigUri(
this ISettings settings,
string environmentVariable,
string configSection,
string configProperty,
Uri defaultValue)
{
if (settings.TryGetSetting(environmentVariable, configSection, configProperty, out string uriStr) &&
Uri.TryCreate(uriStr, UriKind.Absolute, out Uri uri))
{
return uri;
}

return defaultValue;
}
}
}
35 changes: 35 additions & 0 deletions src/shared/Core/JsonHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace GitCredentialManager
{
/// <summary>
/// Helper class providing common JSON serialization options and utilities.
/// </summary>
/// <remarks>
/// The shared JsonSerializerOptions instances should not be modified after initialization
/// to ensure thread safety across the application.
/// </remarks>
public static class JsonHelper
{
/// <summary>
/// Gets JSON serializer options configured for case-insensitive property names.
/// Do not modify this instance; create a new instance if different options are needed.
/// </summary>
public static JsonSerializerOptions CaseInsensitiveOptions { get; } = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};

/// <summary>
/// Gets JSON serializer options configured for case-insensitive property names
/// and ignoring null values when writing.
/// Do not modify this instance; create a new instance if different options are needed.
/// </summary>
public static JsonSerializerOptions CaseInsensitiveIgnoreNullOptions { get; } = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
}
43 changes: 15 additions & 28 deletions src/shared/GitHub/GitHubOAuth2Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,47 +28,34 @@ private static OAuth2ServerEndpoints CreateEndpoints(Uri uri)

private static string GetClientId(ISettings settings)
{
// Check for developer override value
if (settings.TryGetSetting(
return settings.GetOAuthConfigValue(
GitHubConstants.EnvironmentVariables.DevOAuthClientId,
Constants.GitConfiguration.Credential.SectionName, GitHubConstants.GitConfiguration.Credential.DevOAuthClientId,
out string clientId))
{
return clientId;
}

return GitHubConstants.OAuthClientId;
Constants.GitConfiguration.Credential.SectionName,
GitHubConstants.GitConfiguration.Credential.DevOAuthClientId,
GitHubConstants.OAuthClientId);
}

private static Uri GetRedirectUri(ISettings settings, Uri targetUri)
{
// Check for developer override value
if (settings.TryGetSetting(
GitHubConstants.EnvironmentVariables.DevOAuthRedirectUri,
Constants.GitConfiguration.Credential.SectionName, GitHubConstants.GitConfiguration.Credential.DevOAuthRedirectUri,
out string redirectUriStr) && Uri.TryCreate(redirectUriStr, UriKind.Absolute, out Uri redirectUri))
{
return redirectUri;
}

// Only GitHub.com supports the new OAuth redirect URI today
return GitHubHostProvider.IsGitHubDotCom(targetUri)
Uri defaultUri = GitHubHostProvider.IsGitHubDotCom(targetUri)
? GitHubConstants.OAuthRedirectUri
: GitHubConstants.OAuthLegacyRedirectUri;

return settings.GetOAuthConfigUri(
GitHubConstants.EnvironmentVariables.DevOAuthRedirectUri,
Constants.GitConfiguration.Credential.SectionName,
GitHubConstants.GitConfiguration.Credential.DevOAuthRedirectUri,
defaultUri);
}

private static string GetClientSecret(ISettings settings)
{
// Check for developer override value
if (settings.TryGetSetting(
return settings.GetOAuthConfigValue(
GitHubConstants.EnvironmentVariables.DevOAuthClientSecret,
Constants.GitConfiguration.Credential.SectionName, GitHubConstants.GitConfiguration.Credential.DevOAuthClientSecret,
out string clientSecret))
{
return clientSecret;
}

return GitHubConstants.OAuthClientSecret;
Constants.GitConfiguration.Credential.SectionName,
GitHubConstants.GitConfiguration.Credential.DevOAuthClientSecret,
GitHubConstants.OAuthClientSecret);
}
}
}
Loading