11using System ;
2+ using System . Collections . Generic ;
3+ using System . Linq ;
24using System . Net ;
35using System . Threading . Tasks ;
46using GitHub . Extensions ;
7+ using GitHub . Logging ;
58using GitHub . Primitives ;
69using Octokit ;
10+ using Serilog ;
711
812namespace GitHub . Api
913{
@@ -12,11 +16,14 @@ namespace GitHub.Api
1216 /// </summary>
1317 public class LoginManager : ILoginManager
1418 {
15- readonly string [ ] scopes = { "user" , "repo" , "gist" , "write:public_key" } ;
19+ const string ScopesHeader = "X-OAuth-Scopes" ;
20+ static readonly ILogger log = LogManager . ForContext < LoginManager > ( ) ;
21+ static readonly Uri UserEndpoint = new Uri ( "user" , UriKind . Relative ) ;
1622 readonly IKeychain keychain ;
1723 readonly ITwoFactorChallengeHandler twoFactorChallengeHandler ;
1824 readonly string clientId ;
1925 readonly string clientSecret ;
26+ readonly IReadOnlyList < string > scopes ;
2027 readonly string authorizationNote ;
2128 readonly string fingerprint ;
2229
@@ -34,6 +41,7 @@ public LoginManager(
3441 ITwoFactorChallengeHandler twoFactorChallengeHandler ,
3542 string clientId ,
3643 string clientSecret ,
44+ IReadOnlyList < string > scopes ,
3745 string authorizationNote = null ,
3846 string fingerprint = null )
3947 {
@@ -46,6 +54,7 @@ public LoginManager(
4654 this . twoFactorChallengeHandler = twoFactorChallengeHandler ;
4755 this . clientId = clientId ;
4856 this . clientSecret = clientSecret ;
57+ this . scopes = scopes ;
4958 this . authorizationNote = authorizationNote ;
5059 this . fingerprint = fingerprint ;
5160 }
@@ -106,24 +115,7 @@ public async Task<User> Login(
106115 } while ( auth == null ) ;
107116
108117 await keychain . Save ( userName , auth . Token , hostAddress ) . ConfigureAwait ( false ) ;
109-
110- var retry = 0 ;
111-
112- while ( true )
113- {
114- try
115- {
116- return await client . User . Current ( ) . ConfigureAwait ( false ) ;
117- }
118- catch ( AuthorizationException )
119- {
120- if ( retry ++ == 3 ) throw ;
121- }
122-
123- // It seems that attempting to use a token immediately sometimes fails, retry a few
124- // times with a delay of of 1s to allow the token to propagate.
125- await Task . Delay ( 1000 ) ;
126- }
118+ return await ReadUserWithRetry ( client ) ;
127119 }
128120
129121 /// <inheritdoc/>
@@ -132,7 +124,7 @@ public Task<User> LoginFromCache(HostAddress hostAddress, IGitHubClient client)
132124 Guard . ArgumentNotNull ( hostAddress , nameof ( hostAddress ) ) ;
133125 Guard . ArgumentNotNull ( client , nameof ( client ) ) ;
134126
135- return client . User . Current ( ) ;
127+ return ReadUserWithRetry ( client ) ;
136128 }
137129
138130 /// <inheritdoc/>
@@ -258,5 +250,55 @@ bool EnterpriseWorkaround(HostAddress hostAddress, Exception e)
258250 e is ForbiddenException ||
259251 apiException ? . StatusCode == ( HttpStatusCode ) 422 ) ;
260252 }
253+
254+ async Task < User > ReadUserWithRetry ( IGitHubClient client )
255+ {
256+ var retry = 0 ;
257+
258+ while ( true )
259+ {
260+ try
261+ {
262+ return await GetUserAndCheckScopes ( client ) . ConfigureAwait ( false ) ;
263+ }
264+ catch ( AuthorizationException )
265+ {
266+ if ( retry ++ == 3 ) throw ;
267+ }
268+
269+ // It seems that attempting to use a token immediately sometimes fails, retry a few
270+ // times with a delay of of 1s to allow the token to propagate.
271+ await Task . Delay ( 1000 ) ;
272+ }
273+ }
274+
275+ async Task < User > GetUserAndCheckScopes ( IGitHubClient client )
276+ {
277+ var response = await client . Connection . Get < User > (
278+ UserEndpoint , null , null ) . ConfigureAwait ( false ) ;
279+
280+ if ( response . HttpResponse . Headers . ContainsKey ( ScopesHeader ) )
281+ {
282+ var returnedScopes = response . HttpResponse . Headers [ ScopesHeader ]
283+ . Split ( ',' )
284+ . Select ( x => x . Trim ( ) )
285+ . ToArray ( ) ;
286+
287+ if ( scopes . Except ( returnedScopes ) . Count ( ) == 0 )
288+ {
289+ return response . Body ;
290+ }
291+ else
292+ {
293+ log . Error ( "Incorrect API scopes: require {RequiredScopes} but got {Scopes}" , scopes , returnedScopes ) ;
294+ }
295+ }
296+ else
297+ {
298+ log . Error ( "Error reading scopes: /user succeeded but scopes header was not present" ) ;
299+ }
300+
301+ throw new IncorrectScopesException ( ) ;
302+ }
261303 }
262304}
0 commit comments