Skip to content

Commit 96af0f8

Browse files
committed
tidy
1 parent 2538766 commit 96af0f8

File tree

1 file changed

+58
-23
lines changed

1 file changed

+58
-23
lines changed

src/shared/GitLab/GitLabHostProvider.cs

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Net.Http;
43
using System.Threading.Tasks;
54
using GitCredentialManager;
65
using GitCredentialManager.Authentication.OAuth;
6+
using System.Net.Http.Headers;
77

88
namespace GitLab
99
{
@@ -142,33 +142,74 @@ internal AuthenticationModes GetSupportedAuthenticationModes(Uri targetUri)
142142
return AuthenticationModes.Basic | AuthenticationModes.Pat;
143143
}
144144

145-
// <remarks>Stores OAuth refresh token as a side effect</remarks>
145+
// <remarks>Stores OAuth tokens as a side effect</remarks>
146146
public override async Task<ICredential> GetCredentialAsync(InputArguments input)
147147
{
148-
ICredential credential = await base.GetCredentialAsync(input);
149-
if (credential.Account == "oauth2" && credential is not OAuthCredential)
148+
string service = GetServiceName(input);
149+
ICredential credential = Context.CredentialStore.Get(service, input.UserName);
150+
if (credential?.Account == "oauth2" && await IsOAuthTokenExpired(input.GetRemoteUri(), credential.Password))
150151
{
151-
Context.Trace.WriteLine("Retrieved stored OAuth credential");
152-
// retrieved OAuth credential may have expired, so refresh
152+
Context.Trace.WriteLine("Removing expired OAuth access token...");
153+
Context.CredentialStore.Remove(service, credential.Account);
154+
credential = null;
155+
}
156+
157+
if (credential != null)
158+
{
159+
return credential;
160+
}
161+
162+
string refreshService = GetRefreshTokenServiceName(input);
163+
string refreshToken = Context.CredentialStore.Get(service, input.UserName)?.Password;
164+
if (refreshToken != null)
165+
{
166+
Context.Trace.WriteLine("Refreshing OAuth token...");
153167
try
154168
{
155-
credential = await RefreshOAuthCredentialAsync(input);
169+
credential = await RefreshOAuthCredentialAsync(input, refreshToken);
156170
}
157171
catch (Exception e)
158172
{
159173
Context.Terminal.WriteLine($"OAuth token refresh failed: {e.Message}");
160174
}
161175
}
176+
177+
credential ??= await GenerateCredentialAsync(input);
178+
162179
if (credential is OAuthCredential oAuthCredential)
163180
{
181+
Context.Trace.WriteLine("Pre-emptively storing OAuth access and refresh tokens...");
182+
// freshly-generated OAuth credential
183+
// store credential, since we know it to be valid (whereas Git will only store credential if git push succeeds)
184+
Context.CredentialStore.AddOrUpdate(service, oAuthCredential.Account, oAuthCredential.AccessToken);
164185
// store refresh token under a separate service
165-
string refreshTokenService = GetRefreshTokenServiceName(input);
166-
Context.Trace.WriteLine($"Storing credential with service={refreshTokenService} account={input.UserName}...");
167-
Context.CredentialStore.AddOrUpdate(refreshTokenService, oAuthCredential.Account, oAuthCredential.RefreshToken);
186+
Context.CredentialStore.AddOrUpdate(GetRefreshTokenServiceName(input), oAuthCredential.Account, oAuthCredential.RefreshToken);
168187
}
169188
return credential;
170189
}
171190

191+
private async Task<bool> IsOAuthTokenExpired(Uri baseUri, string accessToken)
192+
{
193+
// https://docs.gitlab.com/ee/api/oauth2.html#retrieve-the-token-information
194+
Uri infoUri = new Uri(baseUri, "/oauth/token/info");
195+
using (HttpClient httpClient = Context.HttpClientFactory.CreateClient())
196+
{
197+
httpClient.Timeout = TimeSpan.FromSeconds(15);
198+
httpClient.DefaultRequestHeaders.Authorization
199+
= new AuthenticationHeaderValue("Bearer", accessToken);
200+
try
201+
{
202+
HttpResponseMessage response = await httpClient.GetAsync(infoUri);
203+
return response.StatusCode == System.Net.HttpStatusCode.Unauthorized;
204+
}
205+
catch (Exception e)
206+
{
207+
Context.Terminal.WriteLine($"OAuth token info request failed: {e.Message}");
208+
return false;
209+
}
210+
}
211+
}
212+
172213
internal class OAuthCredential : ICredential
173214
{
174215
public OAuthCredential(OAuth2TokenResult oAuth2TokenResult)
@@ -177,7 +218,7 @@ public OAuthCredential(OAuth2TokenResult oAuth2TokenResult)
177218
RefreshToken = oAuth2TokenResult.RefreshToken;
178219
}
179220

180-
// username must be 'oauth2' https://gitlab.com/gitlab-org/gitlab/-/issues/349461
221+
// username must be 'oauth2' https://docs.gitlab.com/ee/api/oauth2.html#access-git-over-https-with-access-token
181222
public string Account => "oauth2";
182223
public string AccessToken { get; }
183224
public string RefreshToken { get; }
@@ -190,16 +231,8 @@ private async Task<OAuthCredential> GenerateOAuthCredentialAsync(InputArguments
190231
return new OAuthCredential(result);
191232
}
192233

193-
private async Task<OAuthCredential> RefreshOAuthCredentialAsync(InputArguments input)
234+
private async Task<OAuthCredential> RefreshOAuthCredentialAsync(InputArguments input, string refreshToken)
194235
{
195-
// retrieve refresh token stored under separate service
196-
Context.Trace.WriteLine($"Checking for stored refresh token...");
197-
string refreshTokenServiceName = GetRefreshTokenServiceName(input);
198-
string refreshToken = Context.CredentialStore.Get(refreshTokenServiceName, "oauth2").Password;
199-
if (refreshToken == null)
200-
{
201-
throw new InvalidOperationException("No stored refresh token");
202-
}
203236
OAuth2TokenResult result = await _gitLabAuth.GetOAuthTokenViaRefresh(input.GetRemoteUri(), refreshToken);
204237
return new OAuthCredential(result);
205238
}
@@ -212,14 +245,16 @@ protected override void ReleaseManagedResources()
212245

213246
private string GetRefreshTokenServiceName(InputArguments input)
214247
{
215-
return new Uri(new Uri(GetServiceName(input)), "/refresh_token").AbsoluteUri;
248+
var builder = new UriBuilder(GetServiceName(input));
249+
builder.Host = "oauth-refresh-token." + builder.Host;
250+
return builder.Uri.ToString();
216251
}
217252

218253
public override Task EraseCredentialAsync(InputArguments input)
219254
{
220-
Context.CredentialStore.Remove(GetServiceName(input), input.UserName);
255+
// delete any refresh token too
221256
Context.CredentialStore.Remove(GetRefreshTokenServiceName(input), "oauth2");
222-
return Task.CompletedTask;
257+
return base.EraseCredentialAsync(input);
223258
}
224259
}
225260
}

0 commit comments

Comments
 (0)