Skip to content

Commit 717b822

Browse files
committed
generic: add ability to read generic OAuth config
Teach the Generic host provider to read configuration for OAuth-based authentication. These are largely parameters required for the OAuth2Client to be constructed including Client ID/Secret, Redirect URI and Scopes.
1 parent e9ee764 commit 717b822

File tree

5 files changed

+268
-12
lines changed

5 files changed

+268
-12
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
using GitCredentialManager.Tests.Objects;
3+
using Xunit;
4+
5+
namespace GitCredentialManager.Tests
6+
{
7+
public class GenericOAuthConfigTests
8+
{
9+
[Fact]
10+
public void GenericOAuthConfig_TryGet_Valid_ReturnsTrue()
11+
{
12+
var remoteUri = new Uri("https://example.com");
13+
const string expectedClientId = "115845b0-77f8-4c06-a3dc-7d277381fad1";
14+
const string expectedClientSecret = "4D35385D9F24";
15+
const string expectedUserName = "TEST_USER";
16+
const string authzEndpoint = "/oauth/authorize";
17+
const string tokenEndpoint = "/oauth/token";
18+
const string deviceEndpoint = "/oauth/device";
19+
string[] expectedScopes = { "scope1", "scope2" };
20+
var expectedRedirectUri = new Uri("http://localhost:12345");
21+
var expectedAuthzEndpoint = new Uri(remoteUri, authzEndpoint);
22+
var expectedTokenEndpoint = new Uri(remoteUri, tokenEndpoint);
23+
var expectedDeviceEndpoint = new Uri(remoteUri, deviceEndpoint);
24+
25+
string GetKey(string name) => $"{Constants.GitConfiguration.Credential.SectionName}.https://example.com.{name}";
26+
27+
var trace = new NullTrace();
28+
var settings = new TestSettings
29+
{
30+
GitConfiguration = new TestGitConfiguration
31+
{
32+
Global =
33+
{
34+
[GetKey(Constants.GitConfiguration.Credential.OAuthClientId)] = new[] { expectedClientId },
35+
[GetKey(Constants.GitConfiguration.Credential.OAuthClientSecret)] = new[] { expectedClientSecret },
36+
[GetKey(Constants.GitConfiguration.Credential.OAuthRedirectUri)] = new[] { expectedRedirectUri.ToString() },
37+
[GetKey(Constants.GitConfiguration.Credential.OAuthScopes)] = new[] { string.Join(' ', expectedScopes) },
38+
[GetKey(Constants.GitConfiguration.Credential.OAuthAuthzEndpoint)] = new[] { authzEndpoint },
39+
[GetKey(Constants.GitConfiguration.Credential.OAuthTokenEndpoint)] = new[] { tokenEndpoint },
40+
[GetKey(Constants.GitConfiguration.Credential.OAuthDeviceEndpoint)] = new[] { deviceEndpoint },
41+
[GetKey(Constants.GitConfiguration.Credential.OAuthDefaultUserName)] = new[] { expectedUserName },
42+
}
43+
},
44+
RemoteUri = remoteUri
45+
};
46+
47+
bool result = GenericOAuthConfig.TryGet(trace, settings, remoteUri, out GenericOAuthConfig config);
48+
49+
Assert.True(result);
50+
Assert.Equal(expectedClientId, config.ClientId);
51+
Assert.Equal(expectedClientSecret, config.ClientSecret);
52+
Assert.Equal(expectedRedirectUri, config.RedirectUri);
53+
Assert.Equal(expectedScopes, config.Scopes);
54+
Assert.Equal(expectedAuthzEndpoint, config.Endpoints.AuthorizationEndpoint);
55+
Assert.Equal(expectedTokenEndpoint, config.Endpoints.TokenEndpoint);
56+
Assert.Equal(expectedDeviceEndpoint, config.Endpoints.DeviceAuthorizationEndpoint);
57+
Assert.Equal(expectedUserName, config.DefaultUserName);
58+
Assert.True(config.UseAuthHeader);
59+
}
60+
}
61+
}

src/shared/Core/Constants.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ public static class EnvironmentVariables
8989
public const string GcmAutoDetectTimeout = "GCM_AUTODETECT_TIMEOUT";
9090
public const string GcmGuiPromptsEnabled = "GCM_GUI_PROMPT";
9191
public const string GcmUiHelper = "GCM_UI_HELPER";
92+
public const string OAuthAuthenticationModes = "GCM_OAUTH_AUTHMODES";
93+
public const string OAuthClientId = "GCM_OAUTH_CLIENTID";
94+
public const string OAuthClientSecret = "GCM_OAUTH_CLIENTSECRET";
95+
public const string OAuthRedirectUri = "GCM_OAUTH_REDIRECTURI";
96+
public const string OAuthScopes = "GCM_OAUTH_SCOPES";
97+
public const string OAuthAuthzEndpoint = "GCM_OAUTH_AUTHORIZE_ENDPOINT";
98+
public const string OAuthTokenEndpoint = "GCM_OAUTH_TOKEN_ENDPOINT";
99+
public const string OAuthDeviceEndpoint = "GCM_OAUTH_DEVICE_ENDPOINT";
100+
public const string OAuthClientAuthHeader = "GCM_OAUTH_USE_CLIENT_AUTH_HEADER";
101+
public const string OAuthDefaultUserName = "GCM_OAUTH_DEFAULT_USERNAME";
92102
}
93103

94104
public static class Http
@@ -125,6 +135,17 @@ public static class Credential
125135
public const string AutoDetectTimeout = "autoDetectTimeout";
126136
public const string GuiPromptsEnabled = "guiPrompt";
127137
public const string UiHelper = "uiHelper";
138+
139+
public const string OAuthAuthenticationModes = "oauthAuthModes";
140+
public const string OAuthClientId = "oauthClientId";
141+
public const string OAuthClientSecret = "oauthClientSecret";
142+
public const string OAuthRedirectUri = "oauthRedirectUri";
143+
public const string OAuthScopes = "oauthScopes";
144+
public const string OAuthAuthzEndpoint = "oauthAuthorizeEndpoint";
145+
public const string OAuthTokenEndpoint = "oauthTokenEndpoint";
146+
public const string OAuthDeviceEndpoint = "oauthDeviceEndpoint";
147+
public const string OAuthClientAuthHeader = "oauthUseClientAuthHeader";
148+
public const string OAuthDefaultUserName = "oauthDefaultUserName";
128149
}
129150

130151
public static class Http

src/shared/Core/GenericHostProvider.cs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ public GenericHostProvider(ICommandContext context,
2626
_winAuth = winAuth;
2727
}
2828

29-
#region HostProvider
30-
3129
public override string Id => "generic";
3230

3331
public override string Name => "Generic";
@@ -50,12 +48,29 @@ public override async Task<ICredential> GenerateCredentialAsync(InputArguments i
5048

5149
Uri uri = input.GetRemoteUri();
5250

53-
// Determine the if the host supports Windows Integration Authentication (WIA)
51+
// Determine the if the host supports Windows Integration Authentication (WIA) or OAuth
5452
if (!StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "http") &&
5553
!StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "https"))
5654
{
57-
// Cannot check WIA support for non-HTTP based protocols
55+
// Cannot check WIA or OAuth support for non-HTTP based protocols
56+
}
57+
// Check for an OAuth configuration for this remote
58+
else if (GenericOAuthConfig.TryGet(Context.Trace, Context.Settings, uri, out GenericOAuthConfig oauthConfig))
59+
{
60+
Context.Trace.WriteLine($"Found generic OAuth configuration for '{uri}':");
61+
Context.Trace.WriteLine($"\tAuthzEndpoint = {oauthConfig.Endpoints.AuthorizationEndpoint}");
62+
Context.Trace.WriteLine($"\tTokenEndpoint = {oauthConfig.Endpoints.TokenEndpoint}");
63+
Context.Trace.WriteLine($"\tDeviceEndpoint = {oauthConfig.Endpoints.DeviceAuthorizationEndpoint}");
64+
Context.Trace.WriteLine($"\tClientId = {oauthConfig.ClientId}");
65+
Context.Trace.WriteLine($"\tClientSecret = {oauthConfig.ClientSecret}");
66+
Context.Trace.WriteLine($"\tRedirectUri = {oauthConfig.RedirectUri}");
67+
Context.Trace.WriteLine($"\tScopes = [{string.Join(", ", oauthConfig.Scopes)}]");
68+
Context.Trace.WriteLine($"\tUseAuthHeader = {oauthConfig.UseAuthHeader}");
69+
Context.Trace.WriteLine($"\tDefaultUserName = {oauthConfig.DefaultUserName}");
70+
71+
throw new NotImplementedException();
5872
}
73+
// Try detecting WIA for this remote, if permitted
5974
else if (IsWindowsAuthAllowed)
6075
{
6176
if (PlatformUtils.IsWindows())
@@ -86,6 +101,7 @@ public override async Task<ICredential> GenerateCredentialAsync(InputArguments i
86101
Context.Trace.WriteLine("Windows Integrated Authentication detection has been disabled.");
87102
}
88103

104+
// Use basic authentication
89105
Context.Trace.WriteLine("Prompting for basic credentials...");
90106
return await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, input.UserName);
91107
}
@@ -120,7 +136,5 @@ protected override void ReleaseManagedResources()
120136
_winAuth.Dispose();
121137
base.ReleaseManagedResources();
122138
}
123-
124-
#endregion
125139
}
126140
}

src/shared/Core/GenericOAuthConfig.cs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using System;
2+
using GitCredentialManager.Authentication.OAuth;
3+
4+
namespace GitCredentialManager
5+
{
6+
public class GenericOAuthConfig
7+
{
8+
public static bool TryGet(ITrace trace, ISettings settings, Uri remoteUri, out GenericOAuthConfig config)
9+
{
10+
config = new GenericOAuthConfig();
11+
12+
if (!settings.TryGetSetting(
13+
Constants.EnvironmentVariables.OAuthAuthzEndpoint,
14+
Constants.GitConfiguration.Credential.SectionName,
15+
Constants.GitConfiguration.Credential.OAuthAuthzEndpoint,
16+
out string authzEndpoint) ||
17+
!Uri.TryCreate(remoteUri, authzEndpoint, out Uri authzEndpointUri))
18+
{
19+
trace.WriteLine($"Invalid OAuth configuration - missing/invalid authorize endpoint: {authzEndpoint}");
20+
config = null;
21+
return false;
22+
}
23+
24+
if (!settings.TryGetSetting(
25+
Constants.EnvironmentVariables.OAuthTokenEndpoint,
26+
Constants.GitConfiguration.Credential.SectionName,
27+
Constants.GitConfiguration.Credential.OAuthTokenEndpoint,
28+
out string tokenEndpoint) ||
29+
!Uri.TryCreate(remoteUri, tokenEndpoint, out Uri tokenEndpointUri))
30+
{
31+
trace.WriteLine($"Invalid OAuth configuration - missing/invalid token endpoint: {tokenEndpoint}");
32+
config = null;
33+
return false;
34+
}
35+
36+
// Device code endpoint is optional
37+
Uri deviceEndpointUri = null;
38+
if (settings.TryGetSetting(
39+
Constants.EnvironmentVariables.OAuthDeviceEndpoint,
40+
Constants.GitConfiguration.Credential.SectionName,
41+
Constants.GitConfiguration.Credential.OAuthDeviceEndpoint,
42+
out string deviceEndpoint))
43+
{
44+
if (!Uri.TryCreate(remoteUri, deviceEndpoint, out deviceEndpointUri))
45+
{
46+
trace.WriteLine($"Invalid OAuth configuration - invalid device endpoint: {deviceEndpoint}");
47+
}
48+
}
49+
50+
config.Endpoints = new OAuth2ServerEndpoints(authzEndpointUri, tokenEndpointUri)
51+
{
52+
DeviceAuthorizationEndpoint = deviceEndpointUri
53+
};
54+
55+
if (settings.TryGetSetting(
56+
Constants.EnvironmentVariables.OAuthClientId,
57+
Constants.GitConfiguration.Credential.SectionName,
58+
Constants.GitConfiguration.Credential.OAuthClientId,
59+
out string clientId))
60+
{
61+
config.ClientId = clientId;
62+
}
63+
64+
if (settings.TryGetSetting(
65+
Constants.EnvironmentVariables.OAuthClientSecret,
66+
Constants.GitConfiguration.Credential.SectionName,
67+
Constants.GitConfiguration.Credential.OAuthClientSecret,
68+
out string clientSecret))
69+
{
70+
config.ClientSecret = clientSecret;
71+
}
72+
73+
if (settings.TryGetSetting(
74+
Constants.EnvironmentVariables.OAuthRedirectUri,
75+
Constants.GitConfiguration.Credential.SectionName,
76+
Constants.GitConfiguration.Credential.OAuthRedirectUri,
77+
out string redirectUrl) &&
78+
Uri.TryCreate(redirectUrl, UriKind.Absolute, out Uri redirectUri))
79+
{
80+
config.RedirectUri = redirectUri;
81+
}
82+
else
83+
{
84+
trace.WriteLine($"Invalid OAuth configuration - missing/invalid redirect URI: {redirectUrl}");
85+
config = null;
86+
return false;
87+
}
88+
89+
if (settings.TryGetSetting(
90+
Constants.EnvironmentVariables.OAuthScopes,
91+
Constants.GitConfiguration.Credential.SectionName,
92+
Constants.GitConfiguration.Credential.OAuthScopes,
93+
out string scopesStr) && !string.IsNullOrWhiteSpace(scopesStr))
94+
{
95+
config.Scopes = scopesStr.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
96+
}
97+
else
98+
{
99+
config.Scopes = Array.Empty<string>();
100+
}
101+
102+
if (settings.TryGetSetting(
103+
Constants.EnvironmentVariables.OAuthClientAuthHeader,
104+
Constants.GitConfiguration.Credential.SectionName,
105+
Constants.GitConfiguration.Credential.OAuthClientAuthHeader,
106+
out string useHeader))
107+
{
108+
config.UseAuthHeader = useHeader.IsTruthy();
109+
}
110+
else
111+
{
112+
// Default to true
113+
config.UseAuthHeader = true;
114+
}
115+
116+
config.DefaultUserName = settings.TryGetSetting(
117+
Constants.EnvironmentVariables.OAuthDefaultUserName,
118+
Constants.GitConfiguration.Credential.SectionName,
119+
Constants.GitConfiguration.Credential.OAuthDefaultUserName,
120+
out string userName)
121+
? userName
122+
: "OAUTH_USER";
123+
124+
return true;
125+
}
126+
127+
128+
public OAuth2ServerEndpoints Endpoints { get; set; }
129+
public string ClientId { get; set; }
130+
public string ClientSecret { get; set; }
131+
public Uri RedirectUri { get; set; }
132+
public string[] Scopes { get; set; }
133+
public bool UseAuthHeader { get; set; }
134+
public string DefaultUserName { get; set; }
135+
136+
public bool SupportsDeviceCode => Endpoints.DeviceAuthorizationEndpoint != null;
137+
}
138+
}

src/shared/TestInfrastructure/Objects/TestSettings.cs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ public bool TryGetSetting(string envarName, string section, string property, out
5858
return true;
5959
}
6060

61+
if (RemoteUri != null)
62+
{
63+
foreach (string scope in RemoteUri.GetGitConfigurationScopes())
64+
{
65+
string key = $"{section}.{scope}.{property}";
66+
if (GitConfiguration?.TryGet(key, false, out value) ?? false)
67+
{
68+
return true;
69+
}
70+
}
71+
}
72+
6173
if (GitConfiguration?.TryGet($"{section}.{property}", false, out value) ?? false)
6274
{
6375
return true;
@@ -79,16 +91,26 @@ public IEnumerable<string> GetSettingValues(string envarName, string section, st
7991
yield return envarValue;
8092
}
8193

82-
foreach (string scope in RemoteUri.GetGitConfigurationScopes())
94+
IEnumerable<string> configValues;
95+
if (RemoteUri != null)
8396
{
84-
string key = $"{section}.{scope}.{property}";
85-
86-
IEnumerable<string> configValues = GitConfiguration.GetAll(key);
87-
foreach (string value in configValues)
97+
foreach (string scope in RemoteUri.GetGitConfigurationScopes())
8898
{
89-
yield return value;
99+
string key = $"{section}.{scope}.{property}";
100+
101+
configValues = GitConfiguration.GetAll(key);
102+
foreach (string value in configValues)
103+
{
104+
yield return value;
105+
}
90106
}
91107
}
108+
109+
configValues = GitConfiguration.GetAll($"{section}.{property}");
110+
foreach (string value in configValues)
111+
{
112+
yield return value;
113+
}
92114
}
93115

94116
public string RepositoryPath { get; set; }

0 commit comments

Comments
 (0)