async Task <ApplicationAuthorization> HandleTwoFactorAuthorization( HostAddress hostAddress, IGitHubClient client, NewAuthorization newAuth, TwoFactorAuthorizationException exception) { for (;;) { var challengeResult = await twoFactorChallengeHandler.HandleTwoFactorException(exception); if (challengeResult == null) { throw new InvalidOperationException( "ITwoFactorChallengeHandler.HandleTwoFactorException returned null."); } if (!challengeResult.ResendCodeRequested) { try { var auth = await CreateAndDeleteExistingApplicationAuthorization( client, newAuth, challengeResult.AuthenticationCode).ConfigureAwait(false); return(EnsureNonNullAuthorization(auth)); } catch (TwoFactorAuthorizationException e) { exception = e; } catch (Exception e) { await twoFactorChallengeHandler.ChallengeFailed(e); await loginCache.EraseLogin(hostAddress).ConfigureAwait(false); throw; } } else { return(null); } } }
public IObservable <AuthenticationResult> LogIn(string usernameOrEmail, string password) { Guard.ArgumentNotEmptyString(usernameOrEmail, nameof(usernameOrEmail)); Guard.ArgumentNotEmptyString(password, nameof(password)); // If we need to retry on fallback, we'll store the 2FA token // from the first request to re-use: string authenticationCode = null; // We need to intercept the 2FA handler to get the token: var interceptingTwoFactorChallengeHandler = new Func <TwoFactorAuthorizationException, IObservable <TwoFactorChallengeResult> >(ex => twoFactorChallengeHandler.HandleTwoFactorException(ex) .Do(twoFactorChallengeResult => authenticationCode = twoFactorChallengeResult.AuthenticationCode)); // Keep the function to save the authorization token here because it's used // in multiple places in the chain below: var saveAuthorizationToken = new Func <ApplicationAuthorization, IObservable <Unit> >(authorization => { var token = authorization != null ? authorization.Token : null; if (string.IsNullOrWhiteSpace(token)) { return(Observable.Return(Unit.Default)); } return(loginCache.SaveLogin(usernameOrEmail, token, Address) .ObserveOn(RxApp.MainThreadScheduler)); }); // Start be saving the username and password, as they will be used for older versions of Enterprise // that don't support authorization tokens, and for the API client to use until an authorization // token has been created and acquired: return(loginCache.SaveLogin(usernameOrEmail, password, Address) .ObserveOn(RxApp.MainThreadScheduler) // Try to get an authorization token, save it, then get the user to log in: .SelectMany(fingerprint => ApiClient.GetOrCreateApplicationAuthenticationCode(interceptingTwoFactorChallengeHandler)) .SelectMany(saveAuthorizationToken) .SelectMany(_ => GetUserFromApi()) .Catch <AccountCacheItem, ApiException>(firstTryEx => { var exception = firstTryEx as AuthorizationException; if (isEnterprise && exception != null && exception.Message == "Bad credentials") { return Observable.Throw <AccountCacheItem>(exception); } // If the Enterprise host doesn't support the write:public_key scope, it'll return a 422. // EXCEPT, there's a bug where it doesn't, and instead creates a bad token, and in // that case we'd get a 401 here from the GetUser invocation. So to be safe (and consistent // with the Mac app), we'll just retry after any API error for Enterprise hosts: if (isEnterprise && !(firstTryEx is TwoFactorChallengeFailedException)) { // Because we potentially have a bad authorization token due to the Enterprise bug, // we need to reset to using username and password authentication: return loginCache.SaveLogin(usernameOrEmail, password, Address) .ObserveOn(RxApp.MainThreadScheduler) .SelectMany(_ => { // Retry with the old scopes. If we have a stashed 2FA token, we use it: if (authenticationCode != null) { return ApiClient.GetOrCreateApplicationAuthenticationCode( interceptingTwoFactorChallengeHandler, authenticationCode, useOldScopes: true, useFingerprint: false); } // Otherwise, we use the default handler: return ApiClient.GetOrCreateApplicationAuthenticationCode( interceptingTwoFactorChallengeHandler, useOldScopes: true, useFingerprint: false); }) // Then save the authorization token (if there is one) and get the user: .SelectMany(saveAuthorizationToken) .SelectMany(_ => GetUserFromApi()); } return Observable.Throw <AccountCacheItem>(firstTryEx); }) .Catch <AccountCacheItem, ApiException>(retryEx => { // Older Enterprise hosts either don't have the API end-point to PUT an authorization, or they // return 422 because they haven't white-listed our client ID. In that case, we just ignore // the failure, using basic authentication (with username and password) instead of trying // to get an authorization token. // Since enterprise 2.1 and https://github.com/github/github/pull/36669 the API returns 403 // instead of 404 to signal that it's not allowed. In the name of backwards compatibility we // test for both 404 (NotFoundException) and 403 (ForbiddenException) here. if (isEnterprise && (retryEx is NotFoundException || retryEx is ForbiddenException || retryEx.StatusCode == (HttpStatusCode)422)) { return GetUserFromApi(); } // Other errors are "real" so we pass them along: return Observable.Throw <AccountCacheItem>(retryEx); }) .ObserveOn(RxApp.MainThreadScheduler) .Catch <AccountCacheItem, Exception>(ex => { // If we get here, we have an actual login failure: if (ex is TwoFactorChallengeFailedException) { return Observable.Return(unverifiedUser); } if (ex is AuthorizationException) { return Observable.Return(default(AccountCacheItem)); } return Observable.Throw <AccountCacheItem>(ex); }) .SelectMany(LoginWithApiUser) .PublishAsync()); }