-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add OAuth support to generic host provider (#1062)
Add ability to provide OAuth-based authentication for generic hosts by way of simple Git configuration. When a remote URL does not match any known host provider plugin, the generic provider will now first check for OAuth configuration in the Git config or environment variables. If such config is available then we try and perform OAuth authentication. Support for device code flow is optional, and refresh tokens will be used if the service supports and returns them. Users can make use of existing Git config `include` to easily organise and share custom OAuth configurations.
- Loading branch information
Showing
33 changed files
with
1,674 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
# Generic Host Provider OAuth | ||
|
||
Many Git hosts use the popular standard OAuth2 or OpenID Connect (OIDC) | ||
authentication mechanisms to secure repositories they host. | ||
Git Credential Manager supports any generic OAuth2-based Git host by simply | ||
setting some configuration. | ||
|
||
## Registering an OAuth application | ||
|
||
In order to use GCM with a Git host that supports OAuth you must first have | ||
registered an OAuth application with your host. The instructions on how to do | ||
this can be found with your Git host provider's documentation. | ||
|
||
When registering a new application, you should make sure to set an HTTP-based | ||
redirect URL that points to `localhost`; for example: | ||
|
||
```text | ||
http://localhost | ||
http://localhost:<port> | ||
http://127.0.0.1 | ||
http://127.0.0.1:<port> | ||
``` | ||
|
||
Note that you cannot use an HTTPS redirect URL. GCM does not require a specific | ||
port number be used; if your Git host requires you to specify a port number in | ||
the redirect URL then GCM will use that. Otherwise an available port will be | ||
selected at the point authentication starts. | ||
|
||
You must ensure that all scopes required to read and write to Git repositories | ||
have been granted for the application or else credentials that are generated | ||
will cause errors when pushing or fetching using Git. | ||
|
||
As part of the registration process you should also be given a Client ID and, | ||
optionally, a Client Secret. You will need both of these to configure GCM. | ||
|
||
## Configure GCM | ||
|
||
In order to configure GCM to use OAuth with your Git host you need to set the | ||
following values in your Git configuration: | ||
|
||
- Client ID | ||
- Client Secret (optional) | ||
- Redirect URL | ||
- Scopes (optional) | ||
- OAuth Endpoints | ||
- Authorization Endpoint | ||
- Token Endpoint | ||
- Device Code Authorization Endpoint (optional) | ||
|
||
OAuth endpoints can be found by consulting your Git host's OAuth app development | ||
documentation. The URLs can be either absolute or relative to the host name; | ||
for example: `https://example.com/oauth/authorize` or `/oauth/authorize`. | ||
|
||
In order to set these values, you can run the following commands, where `<HOST>` | ||
is the hostname of your Git host: | ||
|
||
```shell | ||
git config --global credential.<HOST>.oauthClientId <ClientID> | ||
git config --global credential.<HOST>.oauthClientSecret <ClientSecret> | ||
git config --global credential.<HOST>.oauthRedirectUri <RedirectURL> | ||
git config --global credential.<HOST>.oauthAuthorizeEndpoint <AuthEndpoint> | ||
git config --global credential.<HOST>.oauthTokenEndpoint <TokenEndpoint> | ||
git config --global credential.<HOST>.oauthScopes <Scopes> | ||
git config --global credential.<HOST>.oauthDeviceEndpoint <DeviceEndpoint> | ||
``` | ||
|
||
**Example commands:** | ||
|
||
- `git config --global credential.https://example.com.oauthClientId C33F2751FB76` | ||
|
||
- `git config --global credential.https://example.com.oauthScopes "code:write profile:read"` | ||
|
||
**Example Git configuration** | ||
|
||
```ini | ||
[credential "https://example.com"] | ||
oauthClientId = 9d886e36-5771-4f2b-8c8b-420c68ad5baa | ||
oauthClientSecret = 4BC5BD4704EAE28FD832 | ||
oauthRedirectUri = "http://127.0.0.1" | ||
oauthAuthorizeEndpoint = "/login/oauth/authorize" | ||
oauthTokenEndpoint = "/login/oauth/token" | ||
oauthDeviceEndpoint = "/login/oauth/device" | ||
oauthScopes = "code:write profile:read" | ||
oauthDefaultUserName = "OAUTH" | ||
oauthUseClientAuthHeader = false | ||
``` | ||
|
||
### Additional configuration | ||
|
||
Depending on the specific implementation of OAuth with your Git host you may | ||
also need to specify additional behavior. | ||
|
||
#### Token user name | ||
|
||
If your Git host requires that you specify a username to use with OAuth tokens | ||
you can either include the username in the Git remote URL, or specify a default | ||
option via Git configuration. | ||
|
||
Example Git remote with username: `https://username@example.com/repo.git`. | ||
In order to use special characters you need to URL encode the values; for | ||
example `@` becomes `%40`. | ||
|
||
By default GCM uses the value `OAUTH-USER` unless specified in the remote URL, | ||
or overriden using the `credential.<HOST>.oauthDefaultUserName` configuration. | ||
|
||
#### Include client authentication in headers | ||
|
||
If your Git host's OAuth implementation has specific requirements about whether | ||
the client ID and secret should or should not be included in an `Authorization` | ||
header during OAuth requests, you can control this using the following setting: | ||
|
||
```shell | ||
git config --global credential.<HOST>.oauthUseClientAuthHeader <true|false> | ||
``` | ||
|
||
The default behavior is to include these values; i.e., `true`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
using System; | ||
using GitCredentialManager.Tests.Objects; | ||
using Xunit; | ||
|
||
namespace GitCredentialManager.Tests | ||
{ | ||
public class GenericOAuthConfigTests | ||
{ | ||
[Fact] | ||
public void GenericOAuthConfig_TryGet_Valid_ReturnsTrue() | ||
{ | ||
var remoteUri = new Uri("https://example.com"); | ||
const string expectedClientId = "115845b0-77f8-4c06-a3dc-7d277381fad1"; | ||
const string expectedClientSecret = "4D35385D9F24"; | ||
const string expectedUserName = "TEST_USER"; | ||
const string authzEndpoint = "/oauth/authorize"; | ||
const string tokenEndpoint = "/oauth/token"; | ||
const string deviceEndpoint = "/oauth/device"; | ||
string[] expectedScopes = { "scope1", "scope2" }; | ||
var expectedRedirectUri = new Uri("http://localhost:12345"); | ||
var expectedAuthzEndpoint = new Uri(remoteUri, authzEndpoint); | ||
var expectedTokenEndpoint = new Uri(remoteUri, tokenEndpoint); | ||
var expectedDeviceEndpoint = new Uri(remoteUri, deviceEndpoint); | ||
|
||
string GetKey(string name) => $"{Constants.GitConfiguration.Credential.SectionName}.https://example.com.{name}"; | ||
|
||
var trace = new NullTrace(); | ||
var settings = new TestSettings | ||
{ | ||
GitConfiguration = new TestGitConfiguration | ||
{ | ||
Global = | ||
{ | ||
[GetKey(Constants.GitConfiguration.Credential.OAuthClientId)] = new[] { expectedClientId }, | ||
[GetKey(Constants.GitConfiguration.Credential.OAuthClientSecret)] = new[] { expectedClientSecret }, | ||
[GetKey(Constants.GitConfiguration.Credential.OAuthRedirectUri)] = new[] { expectedRedirectUri.ToString() }, | ||
[GetKey(Constants.GitConfiguration.Credential.OAuthScopes)] = new[] { string.Join(' ', expectedScopes) }, | ||
[GetKey(Constants.GitConfiguration.Credential.OAuthAuthzEndpoint)] = new[] { authzEndpoint }, | ||
[GetKey(Constants.GitConfiguration.Credential.OAuthTokenEndpoint)] = new[] { tokenEndpoint }, | ||
[GetKey(Constants.GitConfiguration.Credential.OAuthDeviceEndpoint)] = new[] { deviceEndpoint }, | ||
[GetKey(Constants.GitConfiguration.Credential.OAuthDefaultUserName)] = new[] { expectedUserName }, | ||
} | ||
}, | ||
RemoteUri = remoteUri | ||
}; | ||
|
||
bool result = GenericOAuthConfig.TryGet(trace, settings, remoteUri, out GenericOAuthConfig config); | ||
|
||
Assert.True(result); | ||
Assert.Equal(expectedClientId, config.ClientId); | ||
Assert.Equal(expectedClientSecret, config.ClientSecret); | ||
Assert.Equal(expectedRedirectUri, config.RedirectUri); | ||
Assert.Equal(expectedScopes, config.Scopes); | ||
Assert.Equal(expectedAuthzEndpoint, config.Endpoints.AuthorizationEndpoint); | ||
Assert.Equal(expectedTokenEndpoint, config.Endpoints.TokenEndpoint); | ||
Assert.Equal(expectedDeviceEndpoint, config.Endpoints.DeviceAuthorizationEndpoint); | ||
Assert.Equal(expectedUserName, config.DefaultUserName); | ||
Assert.True(config.UseAuthHeader); | ||
} | ||
} | ||
} |
Oops, something went wrong.