66using System . Threading . Tasks ;
77using GitHub . Extensions ;
88using GitHub . Logging ;
9+ using GitHub . Models ;
910using GitHub . Primitives ;
1011using Octokit ;
1112using Serilog ;
@@ -24,7 +25,8 @@ public class LoginManager : ILoginManager
2425 readonly Lazy < ITwoFactorChallengeHandler > twoFactorChallengeHandler ;
2526 readonly string clientId ;
2627 readonly string clientSecret ;
27- readonly IReadOnlyList < string > scopes ;
28+ readonly IReadOnlyList < string > minimumScopes ;
29+ readonly IReadOnlyList < string > requestedScopes ;
2830 readonly string authorizationNote ;
2931 readonly string fingerprint ;
3032 IOAuthCallbackListener oauthListener ;
@@ -37,7 +39,8 @@ public class LoginManager : ILoginManager
3739 /// <param name="oauthListener">The callback listener to signal successful login.</param>
3840 /// <param name="clientId">The application's client API ID.</param>
3941 /// <param name="clientSecret">The application's client API secret.</param>
40- /// <param name="scopes">List of scopes to authenticate for</param>
42+ /// <param name="minimumScopes">The minimum acceptable scopes.</param>
43+ /// <param name="requestedScopes">The scopes to request when logging in.</param>
4144 /// <param name="authorizationNote">An note to store with the authorization.</param>
4245 /// <param name="fingerprint">The machine fingerprint.</param>
4346 public LoginManager (
@@ -46,7 +49,8 @@ public LoginManager(
4649 IOAuthCallbackListener oauthListener ,
4750 string clientId ,
4851 string clientSecret ,
49- IReadOnlyList < string > scopes ,
52+ IReadOnlyList < string > minimumScopes ,
53+ IReadOnlyList < string > requestedScopes ,
5054 string authorizationNote = null ,
5155 string fingerprint = null )
5256 {
@@ -60,13 +64,14 @@ public LoginManager(
6064 this . oauthListener = oauthListener ;
6165 this . clientId = clientId ;
6266 this . clientSecret = clientSecret ;
63- this . scopes = scopes ;
67+ this . minimumScopes = minimumScopes ;
68+ this . requestedScopes = requestedScopes ;
6469 this . authorizationNote = authorizationNote ;
6570 this . fingerprint = fingerprint ;
6671 }
6772
6873 /// <inheritdoc/>
69- public async Task < User > Login (
74+ public async Task < LoginResult > Login (
7075 HostAddress hostAddress ,
7176 IGitHubClient client ,
7277 string userName ,
@@ -83,7 +88,7 @@ public async Task<User> Login(
8388
8489 var newAuth = new NewAuthorization
8590 {
86- Scopes = scopes ,
91+ Scopes = requestedScopes ,
8792 Note = authorizationNote ,
8893 Fingerprint = fingerprint ,
8994 } ;
@@ -121,11 +126,11 @@ public async Task<User> Login(
121126 } while ( auth == null ) ;
122127
123128 await keychain . Save ( userName , auth . Token , hostAddress ) . ConfigureAwait ( false ) ;
124- return await ReadUserWithRetry ( client ) ;
129+ return await ReadUserWithRetry ( client ) . ConfigureAwait ( false ) ;
125130 }
126131
127132 /// <inheritdoc/>
128- public async Task < User > LoginViaOAuth (
133+ public async Task < LoginResult > LoginViaOAuth (
129134 HostAddress hostAddress ,
130135 IGitHubClient client ,
131136 IOauthClient oauthClient ,
@@ -143,18 +148,18 @@ public async Task<User> LoginViaOAuth(
143148
144149 openBrowser ( loginUrl ) ;
145150
146- var code = await listen ;
151+ var code = await listen . ConfigureAwait ( false ) ;
147152 var request = new OauthTokenRequest ( clientId , clientSecret , code ) ;
148- var token = await oauthClient . CreateAccessToken ( request ) ;
153+ var token = await oauthClient . CreateAccessToken ( request ) . ConfigureAwait ( false ) ;
149154
150155 await keychain . Save ( "[oauth]" , token . AccessToken , hostAddress ) . ConfigureAwait ( false ) ;
151- var user = await ReadUserWithRetry ( client ) ;
152- await keychain . Save ( user . Login , token . AccessToken , hostAddress ) . ConfigureAwait ( false ) ;
153- return user ;
156+ var result = await ReadUserWithRetry ( client ) . ConfigureAwait ( false ) ;
157+ await keychain . Save ( result . User . Login , token . AccessToken , hostAddress ) . ConfigureAwait ( false ) ;
158+ return result ;
154159 }
155160
156161 /// <inheritdoc/>
157- public async Task < User > LoginWithToken (
162+ public async Task < LoginResult > LoginWithToken (
158163 HostAddress hostAddress ,
159164 IGitHubClient client ,
160165 string token )
@@ -167,19 +172,19 @@ public async Task<User> LoginWithToken(
167172
168173 try
169174 {
170- var user = await ReadUserWithRetry ( client ) ;
171- await keychain . Save ( user . Login , token , hostAddress ) . ConfigureAwait ( false ) ;
172- return user ;
175+ var result = await ReadUserWithRetry ( client ) . ConfigureAwait ( false ) ;
176+ await keychain . Save ( result . User . Login , token , hostAddress ) . ConfigureAwait ( false ) ;
177+ return result ;
173178 }
174179 catch
175180 {
176- await keychain . Delete ( hostAddress ) ;
181+ await keychain . Delete ( hostAddress ) . ConfigureAwait ( false ) ;
177182 throw ;
178183 }
179184 }
180185
181186 /// <inheritdoc/>
182- public Task < User > LoginFromCache ( HostAddress hostAddress , IGitHubClient client )
187+ public Task < LoginResult > LoginFromCache ( HostAddress hostAddress , IGitHubClient client )
183188 {
184189 Guard . ArgumentNotNull ( hostAddress , nameof ( hostAddress ) ) ;
185190 Guard . ArgumentNotNull ( client , nameof ( client ) ) ;
@@ -193,41 +198,7 @@ public async Task Logout(HostAddress hostAddress, IGitHubClient client)
193198 Guard . ArgumentNotNull ( hostAddress , nameof ( hostAddress ) ) ;
194199 Guard . ArgumentNotNull ( client , nameof ( client ) ) ;
195200
196- await keychain . Delete ( hostAddress ) ;
197- }
198-
199- /// <summary>
200- /// Tests if received API scopes match the required API scopes.
201- /// </summary>
202- /// <param name="required">The required API scopes.</param>
203- /// <param name="received">The received API scopes.</param>
204- /// <returns>True if all required scopes are present, otherwise false.</returns>
205- public static bool ScopesMatch ( IReadOnlyList < string > required , IReadOnlyList < string > received )
206- {
207- foreach ( var scope in required )
208- {
209- var found = received . Contains ( scope ) ;
210-
211- if ( ! found &&
212- ( scope . StartsWith ( "read:" , StringComparison . Ordinal ) ||
213- scope . StartsWith ( "write:" , StringComparison . Ordinal ) ) )
214- {
215- // NOTE: Scopes are actually more complex than this, for example
216- // `user` encompasses `read:user` and `user:email` but just use
217- // this simple rule for now as it works for the scopes we require.
218- var adminScope = scope
219- . Replace ( "read:" , "admin:" )
220- . Replace ( "write:" , "admin:" ) ;
221- found = received . Contains ( adminScope ) ;
222- }
223-
224- if ( ! found )
225- {
226- return false ;
227- }
228- }
229-
230- return true ;
201+ await keychain . Delete ( hostAddress ) . ConfigureAwait ( false ) ;
231202 }
232203
233204 async Task < ApplicationAuthorization > CreateAndDeleteExistingApplicationAuthorization (
@@ -256,18 +227,18 @@ async Task<ApplicationAuthorization> CreateAndDeleteExistingApplicationAuthoriza
256227 twoFactorAuthenticationCode ) . ConfigureAwait ( false ) ;
257228 }
258229
259- if ( result . Token == string . Empty )
230+ if ( string . IsNullOrEmpty ( result . Token ) )
260231 {
261232 if ( twoFactorAuthenticationCode == null )
262233 {
263- await client . Authorization . Delete ( result . Id ) ;
234+ await client . Authorization . Delete ( result . Id ) . ConfigureAwait ( false ) ;
264235 }
265236 else
266237 {
267- await client . Authorization . Delete ( result . Id , twoFactorAuthenticationCode ) ;
238+ await client . Authorization . Delete ( result . Id , twoFactorAuthenticationCode ) . ConfigureAwait ( false ) ;
268239 }
269240 }
270- } while ( result . Token == string . Empty && retry ++ == 0 ) ;
241+ } while ( string . IsNullOrEmpty ( result . Token ) && retry ++ == 0 ) ;
271242
272243 return result ;
273244 }
@@ -280,7 +251,7 @@ async Task<ApplicationAuthorization> HandleTwoFactorAuthorization(
280251 {
281252 for ( ; ; )
282253 {
283- var challengeResult = await twoFactorChallengeHandler . Value . HandleTwoFactorException ( exception ) ;
254+ var challengeResult = await twoFactorChallengeHandler . Value . HandleTwoFactorException ( exception ) . ConfigureAwait ( false ) ;
284255
285256 if ( challengeResult == null )
286257 {
@@ -304,7 +275,7 @@ async Task<ApplicationAuthorization> HandleTwoFactorAuthorization(
304275 }
305276 catch ( Exception e )
306277 {
307- await twoFactorChallengeHandler . Value . ChallengeFailed ( e ) ;
278+ await twoFactorChallengeHandler . Value . ChallengeFailed ( e ) . ConfigureAwait ( false ) ;
308279 await keychain . Delete ( hostAddress ) . ConfigureAwait ( false ) ;
309280 throw ;
310281 }
@@ -345,7 +316,7 @@ e is ForbiddenException ||
345316 apiException ? . StatusCode == ( HttpStatusCode ) 422 ) ;
346317 }
347318
348- async Task < User > ReadUserWithRetry ( IGitHubClient client )
319+ async Task < LoginResult > ReadUserWithRetry ( IGitHubClient client )
349320 {
350321 var retry = 0 ;
351322
@@ -362,29 +333,29 @@ async Task<User> ReadUserWithRetry(IGitHubClient client)
362333
363334 // It seems that attempting to use a token immediately sometimes fails, retry a few
364335 // times with a delay of of 1s to allow the token to propagate.
365- await Task . Delay ( 1000 ) ;
336+ await Task . Delay ( 1000 ) . ConfigureAwait ( false ) ;
366337 }
367338 }
368339
369- async Task < User > GetUserAndCheckScopes ( IGitHubClient client )
340+ async Task < LoginResult > GetUserAndCheckScopes ( IGitHubClient client )
370341 {
371342 var response = await client . Connection . Get < User > (
372343 UserEndpoint , null , null ) . ConfigureAwait ( false ) ;
373344
374345 if ( response . HttpResponse . Headers . ContainsKey ( ScopesHeader ) )
375346 {
376- var returnedScopes = response . HttpResponse . Headers [ ScopesHeader ]
347+ var returnedScopes = new ScopesCollection ( response . HttpResponse . Headers [ ScopesHeader ]
377348 . Split ( ',' )
378349 . Select ( x => x . Trim ( ) )
379- . ToArray ( ) ;
350+ . ToArray ( ) ) ;
380351
381- if ( ScopesMatch ( scopes , returnedScopes ) )
352+ if ( returnedScopes . Matches ( minimumScopes ) )
382353 {
383- return response . Body ;
354+ return new LoginResult ( response . Body , returnedScopes ) ;
384355 }
385356 else
386357 {
387- log . Error ( "Incorrect API scopes: require {RequiredScopes} but got {Scopes}" , scopes , returnedScopes ) ;
358+ log . Error ( "Incorrect API scopes: require {RequiredScopes} but got {Scopes}" , minimumScopes , returnedScopes ) ;
388359 }
389360 }
390361 else
@@ -393,7 +364,7 @@ async Task<User> GetUserAndCheckScopes(IGitHubClient client)
393364 }
394365
395366 throw new IncorrectScopesException (
396- "Incorrect API scopes. Required: " + string . Join ( "," , scopes ) ) ;
367+ "Incorrect API scopes. Required: " + string . Join ( "," , minimumScopes ) ) ;
397368 }
398369
399370 Uri GetLoginUrl ( IOauthClient client , string state )
@@ -402,7 +373,7 @@ Uri GetLoginUrl(IOauthClient client, string state)
402373
403374 request . State = state ;
404375
405- foreach ( var scope in scopes )
376+ foreach ( var scope in requestedScopes )
406377 {
407378 request . Scopes . Add ( scope ) ;
408379 }
0 commit comments