@@ -116,7 +116,7 @@ public override async Task<ICredential> GenerateCredentialAsync(InputArguments i
116
116
return promptResult . Credential ;
117
117
118
118
case AuthenticationModes . Browser :
119
- return await GenerateOAuthCredentialAsync ( remoteUri ) ;
119
+ return await GenerateOAuthCredentialAsync ( input ) ;
120
120
121
121
default :
122
122
throw new ArgumentOutOfRangeException ( nameof ( promptResult ) ) ;
@@ -152,18 +152,75 @@ internal AuthenticationModes GetSupportedAuthenticationModes(Uri targetUri)
152
152
return AuthenticationModes . Basic | AuthenticationModes . Pat ;
153
153
}
154
154
155
- private async Task < GitCredential > GenerateOAuthCredentialAsync ( Uri targetUri )
155
+ public override async Task < ICredential > GetCredentialAsync ( InputArguments input )
156
156
{
157
- OAuth2TokenResult result = await _gitLabAuth . GetOAuthTokenViaBrowserAsync ( targetUri , GitLabOAuthScopes ) ;
157
+ ICredential credential = await base . GetCredentialAsync ( input ) ;
158
+ if ( credential . Account == "oauth2" )
159
+ {
160
+ // cast succeeds if and only if credential is freshly generated (not retrieved)
161
+ OAuthCredential oAuthCredential = credential as OAuthCredential ;
162
+ if ( oAuthCredential == null )
163
+ {
164
+ // retrieved OAuth credential may have expired, so refresh
165
+ try
166
+ {
167
+ oAuthCredential = await RefreshOAuthCredentialAsync ( input ) ;
168
+ }
169
+ catch ( Exception e )
170
+ {
171
+ Context . Terminal . WriteLine ( $ "OAuth token refresh failed: { e . Message } ") ;
172
+ return credential ;
173
+ }
174
+ }
175
+ // store refresh token under a separate service
176
+ Context . Trace . WriteLine ( "Storing refresh token..." ) ;
177
+ Context . CredentialStore . AddOrUpdate ( GetRefreshTokenServiceName ( input . GetRemoteUri ( ) ) , "oauth2" , oAuthCredential . RefreshToken ) ;
178
+ return oAuthCredential ;
179
+ }
158
180
159
- // username oauth2 https://gitlab.com/gitlab-org/gitlab/-/issues/349461
160
- return new GitCredential ( "oauth2" , result . AccessToken ) ;
181
+ return credential ;
182
+ }
183
+
184
+ internal class OAuthCredential : GitCredential
185
+ {
186
+ // username must be oauth2 https://gitlab.com/gitlab-org/gitlab/-/issues/349461
187
+ public OAuthCredential ( OAuth2TokenResult oAuth2TokenResult ) : base ( "oauth2" , oAuth2TokenResult . AccessToken )
188
+ {
189
+ RefreshToken = oAuth2TokenResult . RefreshToken ;
190
+ }
191
+
192
+ public string RefreshToken { get ; }
193
+ }
194
+
195
+ private async Task < OAuthCredential > GenerateOAuthCredentialAsync ( InputArguments input )
196
+ {
197
+ OAuth2TokenResult result = await _gitLabAuth . GetOAuthTokenViaBrowserAsync ( input . GetRemoteUri ( ) , GitLabOAuthScopes ) ;
198
+ return new OAuthCredential ( result ) ;
199
+ }
200
+
201
+ private async Task < OAuthCredential > RefreshOAuthCredentialAsync ( InputArguments input )
202
+ {
203
+ // retrieve refresh token stored under separate service
204
+ Context . Trace . WriteLine ( $ "Checking for stored refresh token...") ;
205
+ string refreshTokenServiceName = GetRefreshTokenServiceName ( input . GetRemoteUri ( ) ) ;
206
+ string refreshToken = Context . CredentialStore . Get ( refreshTokenServiceName , "oauth2" ) . Password ;
207
+ if ( refreshToken == null )
208
+ {
209
+ throw new InvalidOperationException ( "No stored refresh token" ) ;
210
+ }
211
+ OAuth2TokenResult result = await _gitLabAuth . GetOAuthTokenViaRefresh ( input . GetRemoteUri ( ) , refreshToken ) ;
212
+ return new OAuthCredential ( result ) ;
161
213
}
162
214
163
215
protected override void ReleaseManagedResources ( )
164
216
{
165
217
_gitLabAuth . Dispose ( ) ;
166
218
base . ReleaseManagedResources ( ) ;
167
219
}
220
+
221
+ private static string GetRefreshTokenServiceName ( Uri baseUri )
222
+ {
223
+ return new Uri ( baseUri . WithoutUserInfo ( ) , "/refresh_token" ) . AbsoluteUri ;
224
+ }
168
225
}
169
226
}
0 commit comments