1
1
using System ;
2
- using System . Collections . Generic ;
3
2
using System . Net . Http ;
4
3
using System . Threading . Tasks ;
5
4
using GitCredentialManager ;
6
5
using GitCredentialManager . Authentication . OAuth ;
6
+ using System . Net . Http . Headers ;
7
7
8
8
namespace GitLab
9
9
{
@@ -142,33 +142,74 @@ internal AuthenticationModes GetSupportedAuthenticationModes(Uri targetUri)
142
142
return AuthenticationModes . Basic | AuthenticationModes . Pat ;
143
143
}
144
144
145
- // <remarks>Stores OAuth refresh token as a side effect</remarks>
145
+ // <remarks>Stores OAuth tokens as a side effect</remarks>
146
146
public override async Task < ICredential > GetCredentialAsync ( InputArguments input )
147
147
{
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 ) )
150
151
{
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..." ) ;
153
167
try
154
168
{
155
- credential = await RefreshOAuthCredentialAsync ( input ) ;
169
+ credential = await RefreshOAuthCredentialAsync ( input , refreshToken ) ;
156
170
}
157
171
catch ( Exception e )
158
172
{
159
173
Context . Terminal . WriteLine ( $ "OAuth token refresh failed: { e . Message } ") ;
160
174
}
161
175
}
176
+
177
+ credential ??= await GenerateCredentialAsync ( input ) ;
178
+
162
179
if ( credential is OAuthCredential oAuthCredential )
163
180
{
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 ) ;
164
185
// 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 ) ;
168
187
}
169
188
return credential ;
170
189
}
171
190
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
+
172
213
internal class OAuthCredential : ICredential
173
214
{
174
215
public OAuthCredential ( OAuth2TokenResult oAuth2TokenResult )
@@ -177,7 +218,7 @@ public OAuthCredential(OAuth2TokenResult oAuth2TokenResult)
177
218
RefreshToken = oAuth2TokenResult . RefreshToken ;
178
219
}
179
220
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
181
222
public string Account => "oauth2" ;
182
223
public string AccessToken { get ; }
183
224
public string RefreshToken { get ; }
@@ -190,16 +231,8 @@ private async Task<OAuthCredential> GenerateOAuthCredentialAsync(InputArguments
190
231
return new OAuthCredential ( result ) ;
191
232
}
192
233
193
- private async Task < OAuthCredential > RefreshOAuthCredentialAsync ( InputArguments input )
234
+ private async Task < OAuthCredential > RefreshOAuthCredentialAsync ( InputArguments input , string refreshToken )
194
235
{
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
- }
203
236
OAuth2TokenResult result = await _gitLabAuth . GetOAuthTokenViaRefresh ( input . GetRemoteUri ( ) , refreshToken ) ;
204
237
return new OAuthCredential ( result ) ;
205
238
}
@@ -212,14 +245,16 @@ protected override void ReleaseManagedResources()
212
245
213
246
private string GetRefreshTokenServiceName ( InputArguments input )
214
247
{
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 ( ) ;
216
251
}
217
252
218
253
public override Task EraseCredentialAsync ( InputArguments input )
219
254
{
220
- Context . CredentialStore . Remove ( GetServiceName ( input ) , input . UserName ) ;
255
+ // delete any refresh token too
221
256
Context . CredentialStore . Remove ( GetRefreshTokenServiceName ( input ) , "oauth2" ) ;
222
- return Task . CompletedTask ;
257
+ return base . EraseCredentialAsync ( input ) ;
223
258
}
224
259
}
225
260
}
0 commit comments