6
6
using System . Threading . Tasks ;
7
7
using GitHub . Extensions ;
8
8
using GitHub . Logging ;
9
+ using GitHub . Models ;
9
10
using GitHub . Primitives ;
10
11
using Octokit ;
11
12
using Serilog ;
@@ -24,7 +25,8 @@ public class LoginManager : ILoginManager
24
25
readonly Lazy < ITwoFactorChallengeHandler > twoFactorChallengeHandler ;
25
26
readonly string clientId ;
26
27
readonly string clientSecret ;
27
- readonly IReadOnlyList < string > scopes ;
28
+ readonly IReadOnlyList < string > minimumScopes ;
29
+ readonly IReadOnlyList < string > requestedScopes ;
28
30
readonly string authorizationNote ;
29
31
readonly string fingerprint ;
30
32
IOAuthCallbackListener oauthListener ;
@@ -37,7 +39,8 @@ public class LoginManager : ILoginManager
37
39
/// <param name="oauthListener">The callback listener to signal successful login.</param>
38
40
/// <param name="clientId">The application's client API ID.</param>
39
41
/// <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>
41
44
/// <param name="authorizationNote">An note to store with the authorization.</param>
42
45
/// <param name="fingerprint">The machine fingerprint.</param>
43
46
public LoginManager (
@@ -46,7 +49,8 @@ public LoginManager(
46
49
IOAuthCallbackListener oauthListener ,
47
50
string clientId ,
48
51
string clientSecret ,
49
- IReadOnlyList < string > scopes ,
52
+ IReadOnlyList < string > minimumScopes ,
53
+ IReadOnlyList < string > requestedScopes ,
50
54
string authorizationNote = null ,
51
55
string fingerprint = null )
52
56
{
@@ -60,13 +64,14 @@ public LoginManager(
60
64
this . oauthListener = oauthListener ;
61
65
this . clientId = clientId ;
62
66
this . clientSecret = clientSecret ;
63
- this . scopes = scopes ;
67
+ this . minimumScopes = minimumScopes ;
68
+ this . requestedScopes = requestedScopes ;
64
69
this . authorizationNote = authorizationNote ;
65
70
this . fingerprint = fingerprint ;
66
71
}
67
72
68
73
/// <inheritdoc/>
69
- public async Task < User > Login (
74
+ public async Task < LoginResult > Login (
70
75
HostAddress hostAddress ,
71
76
IGitHubClient client ,
72
77
string userName ,
@@ -83,7 +88,7 @@ public async Task<User> Login(
83
88
84
89
var newAuth = new NewAuthorization
85
90
{
86
- Scopes = scopes ,
91
+ Scopes = requestedScopes ,
87
92
Note = authorizationNote ,
88
93
Fingerprint = fingerprint ,
89
94
} ;
@@ -121,11 +126,11 @@ public async Task<User> Login(
121
126
} while ( auth == null ) ;
122
127
123
128
await keychain . Save ( userName , auth . Token , hostAddress ) . ConfigureAwait ( false ) ;
124
- return await ReadUserWithRetry ( client ) ;
129
+ return await ReadUserWithRetry ( client ) . ConfigureAwait ( false ) ;
125
130
}
126
131
127
132
/// <inheritdoc/>
128
- public async Task < User > LoginViaOAuth (
133
+ public async Task < LoginResult > LoginViaOAuth (
129
134
HostAddress hostAddress ,
130
135
IGitHubClient client ,
131
136
IOauthClient oauthClient ,
@@ -143,18 +148,18 @@ public async Task<User> LoginViaOAuth(
143
148
144
149
openBrowser ( loginUrl ) ;
145
150
146
- var code = await listen ;
151
+ var code = await listen . ConfigureAwait ( false ) ;
147
152
var request = new OauthTokenRequest ( clientId , clientSecret , code ) ;
148
- var token = await oauthClient . CreateAccessToken ( request ) ;
153
+ var token = await oauthClient . CreateAccessToken ( request ) . ConfigureAwait ( false ) ;
149
154
150
155
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 ;
154
159
}
155
160
156
161
/// <inheritdoc/>
157
- public async Task < User > LoginWithToken (
162
+ public async Task < LoginResult > LoginWithToken (
158
163
HostAddress hostAddress ,
159
164
IGitHubClient client ,
160
165
string token )
@@ -167,19 +172,19 @@ public async Task<User> LoginWithToken(
167
172
168
173
try
169
174
{
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 ;
173
178
}
174
179
catch
175
180
{
176
- await keychain . Delete ( hostAddress ) ;
181
+ await keychain . Delete ( hostAddress ) . ConfigureAwait ( false ) ;
177
182
throw ;
178
183
}
179
184
}
180
185
181
186
/// <inheritdoc/>
182
- public Task < User > LoginFromCache ( HostAddress hostAddress , IGitHubClient client )
187
+ public Task < LoginResult > LoginFromCache ( HostAddress hostAddress , IGitHubClient client )
183
188
{
184
189
Guard . ArgumentNotNull ( hostAddress , nameof ( hostAddress ) ) ;
185
190
Guard . ArgumentNotNull ( client , nameof ( client ) ) ;
@@ -193,41 +198,7 @@ public async Task Logout(HostAddress hostAddress, IGitHubClient client)
193
198
Guard . ArgumentNotNull ( hostAddress , nameof ( hostAddress ) ) ;
194
199
Guard . ArgumentNotNull ( client , nameof ( client ) ) ;
195
200
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 ) ;
231
202
}
232
203
233
204
async Task < ApplicationAuthorization > CreateAndDeleteExistingApplicationAuthorization (
@@ -256,18 +227,18 @@ async Task<ApplicationAuthorization> CreateAndDeleteExistingApplicationAuthoriza
256
227
twoFactorAuthenticationCode ) . ConfigureAwait ( false ) ;
257
228
}
258
229
259
- if ( result . Token == string . Empty )
230
+ if ( string . IsNullOrEmpty ( result . Token ) )
260
231
{
261
232
if ( twoFactorAuthenticationCode == null )
262
233
{
263
- await client . Authorization . Delete ( result . Id ) ;
234
+ await client . Authorization . Delete ( result . Id ) . ConfigureAwait ( false ) ;
264
235
}
265
236
else
266
237
{
267
- await client . Authorization . Delete ( result . Id , twoFactorAuthenticationCode ) ;
238
+ await client . Authorization . Delete ( result . Id , twoFactorAuthenticationCode ) . ConfigureAwait ( false ) ;
268
239
}
269
240
}
270
- } while ( result . Token == string . Empty && retry ++ == 0 ) ;
241
+ } while ( string . IsNullOrEmpty ( result . Token ) && retry ++ == 0 ) ;
271
242
272
243
return result ;
273
244
}
@@ -280,7 +251,7 @@ async Task<ApplicationAuthorization> HandleTwoFactorAuthorization(
280
251
{
281
252
for ( ; ; )
282
253
{
283
- var challengeResult = await twoFactorChallengeHandler . Value . HandleTwoFactorException ( exception ) ;
254
+ var challengeResult = await twoFactorChallengeHandler . Value . HandleTwoFactorException ( exception ) . ConfigureAwait ( false ) ;
284
255
285
256
if ( challengeResult == null )
286
257
{
@@ -304,7 +275,7 @@ async Task<ApplicationAuthorization> HandleTwoFactorAuthorization(
304
275
}
305
276
catch ( Exception e )
306
277
{
307
- await twoFactorChallengeHandler . Value . ChallengeFailed ( e ) ;
278
+ await twoFactorChallengeHandler . Value . ChallengeFailed ( e ) . ConfigureAwait ( false ) ;
308
279
await keychain . Delete ( hostAddress ) . ConfigureAwait ( false ) ;
309
280
throw ;
310
281
}
@@ -345,7 +316,7 @@ e is ForbiddenException ||
345
316
apiException ? . StatusCode == ( HttpStatusCode ) 422 ) ;
346
317
}
347
318
348
- async Task < User > ReadUserWithRetry ( IGitHubClient client )
319
+ async Task < LoginResult > ReadUserWithRetry ( IGitHubClient client )
349
320
{
350
321
var retry = 0 ;
351
322
@@ -362,29 +333,29 @@ async Task<User> ReadUserWithRetry(IGitHubClient client)
362
333
363
334
// It seems that attempting to use a token immediately sometimes fails, retry a few
364
335
// 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 ) ;
366
337
}
367
338
}
368
339
369
- async Task < User > GetUserAndCheckScopes ( IGitHubClient client )
340
+ async Task < LoginResult > GetUserAndCheckScopes ( IGitHubClient client )
370
341
{
371
342
var response = await client . Connection . Get < User > (
372
343
UserEndpoint , null , null ) . ConfigureAwait ( false ) ;
373
344
374
345
if ( response . HttpResponse . Headers . ContainsKey ( ScopesHeader ) )
375
346
{
376
- var returnedScopes = response . HttpResponse . Headers [ ScopesHeader ]
347
+ var returnedScopes = new ScopesCollection ( response . HttpResponse . Headers [ ScopesHeader ]
377
348
. Split ( ',' )
378
349
. Select ( x => x . Trim ( ) )
379
- . ToArray ( ) ;
350
+ . ToArray ( ) ) ;
380
351
381
- if ( ScopesMatch ( scopes , returnedScopes ) )
352
+ if ( returnedScopes . Matches ( minimumScopes ) )
382
353
{
383
- return response . Body ;
354
+ return new LoginResult ( response . Body , returnedScopes ) ;
384
355
}
385
356
else
386
357
{
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 ) ;
388
359
}
389
360
}
390
361
else
@@ -393,7 +364,7 @@ async Task<User> GetUserAndCheckScopes(IGitHubClient client)
393
364
}
394
365
395
366
throw new IncorrectScopesException (
396
- "Incorrect API scopes. Required: " + string . Join ( "," , scopes ) ) ;
367
+ "Incorrect API scopes. Required: " + string . Join ( "," , minimumScopes ) ) ;
397
368
}
398
369
399
370
Uri GetLoginUrl ( IOauthClient client , string state )
@@ -402,7 +373,7 @@ Uri GetLoginUrl(IOauthClient client, string state)
402
373
403
374
request . State = state ;
404
375
405
- foreach ( var scope in scopes )
376
+ foreach ( var scope in requestedScopes )
406
377
{
407
378
request . Scopes . Add ( scope ) ;
408
379
}
0 commit comments