diff --git a/src/managed/OpenLiveWriter.BlogClient/Clients/GoogleBloggerv3Client.cs b/src/managed/OpenLiveWriter.BlogClient/Clients/GoogleBloggerv3Client.cs index 87369661..9f00d937 100644 --- a/src/managed/OpenLiveWriter.BlogClient/Clients/GoogleBloggerv3Client.cs +++ b/src/managed/OpenLiveWriter.BlogClient/Clients/GoogleBloggerv3Client.cs @@ -18,6 +18,8 @@ using Google.Apis.Util.Store; using Google.Apis.Services; using Google.Apis.Auth.OAuth2.Flows; using OpenLiveWriter.BlogClient.Providers; +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Util; namespace OpenLiveWriter.BlogClient.Clients { @@ -55,8 +57,7 @@ namespace OpenLiveWriter.BlogClient.Clients private static IDataStore GetCredentialsDataStoreForBlog(string blogId) { - // The Google APIs will automatically store the OAuth2 tokens in the given path. We use a unique path per - // blog to support multiple Blogger accounts. + // The Google APIs will automatically store the OAuth2 tokens in the given path. var folderPath = Path.Combine(ApplicationEnvironment.LocalApplicationDataDirectory, "GoogleBloggerv3"); return new FileDataStore(folderPath, true); } @@ -88,9 +89,26 @@ namespace OpenLiveWriter.BlogClient.Clients _clientOptions = clientOptions; } + private BloggerService GetService() + { + TransientCredentials transientCredentials = Login(); + return new BloggerService(new BaseClientService.Initializer() + { + HttpClientInitializer = (UserCredential)transientCredentials.Token + }); + } + + private bool IsValidToken(TokenResponse token) + { + // If the token is expired but we have a non-null RefreshToken, we can assume the token will be + // automatically refreshed when we query Google Blogger and is therefore valid. + return token != null && (!token.IsExpired(SystemClock.Default) || token.RefreshToken != null); + } + protected override TransientCredentials Login() { - TransientCredentials transientCredentials = Credentials.TransientCredentials as TransientCredentials; + var transientCredentials = Credentials.TransientCredentials as TransientCredentials ?? + new TransientCredentials(Credentials.Username, Credentials.Password, null); VerifyAndRefreshCredentials(transientCredentials); return transientCredentials; } @@ -102,39 +120,64 @@ namespace OpenLiveWriter.BlogClient.Clients private void VerifyAndRefreshCredentials(TransientCredentials tc) { - var flowInitializer = new GoogleAuthorizationCodeFlow.Initializer - { - ClientSecretsStream = ClientSecretsStream, - DataStore = GetCredentialsDataStoreForBlog(tc.Username), - Scopes = new List() { BloggerServiceScope, PicasaServiceScope }, - }; - var flow = new GoogleAuthorizationCodeFlow(flowInitializer); + var userCredential = tc.Token as UserCredential; + var token = userCredential?.Token; var cancellationTokenSource = new CancellationTokenSource(); - // Attempt to load a cached OAuth token. - var loadTokenTask = flow.LoadTokenAsync(tc.Username, cancellationTokenSource.Token); - loadTokenTask.Wait(); - if (loadTokenTask.IsCompleted) + if (IsValidToken(token)) { - var token = loadTokenTask.Result; - if (token == null || (token.RefreshToken == null && token.IsExpired(flow.Clock))) + // We already have a valid OAuth token. + return; + } + + if (userCredential == null) + { + // Attempt to load a cached OAuth token. + var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer { - // The token is invalid, so we need to login again. - if (BlogClientUIContext.SilentModeForCurrentThread) - { - // If we're in silent mode where prompting isn't allowed, throw the verification exception - throw new BlogClientAuthenticationException(String.Empty, String.Empty); - } + ClientSecretsStream = ClientSecretsStream, + DataStore = GetCredentialsDataStoreForBlog(tc.Username), + Scopes = new List() { BloggerServiceScope, PicasaServiceScope }, + }); - var authorizationTask = GetOAuth2AuthorizationAsync(tc.Username, cancellationTokenSource.Token); - authorizationTask.Wait(); - - if (authorizationTask.Result?.Token == null) - { - throw new BlogClientOperationCancelledException(); - } + var loadTokenTask = flow.LoadTokenAsync(tc.Username, cancellationTokenSource.Token); + loadTokenTask.Wait(); + if (loadTokenTask.IsCompleted) + { + // We were able re-create the user credentials from the cache. + userCredential = new UserCredential(flow, tc.Username, loadTokenTask.Result); + token = loadTokenTask.Result; } } + + if (!IsValidToken(token)) + { + // The token is invalid, so we need to login again. This likely includes popping out a new browser window. + if (BlogClientUIContext.SilentModeForCurrentThread) + { + // If we're in silent mode where prompting isn't allowed, throw the verification exception + throw new BlogClientAuthenticationException(String.Empty, String.Empty); + } + + // Start an OAuth flow to renew the credentials. + var authorizationTask = GetOAuth2AuthorizationAsync(tc.Username, cancellationTokenSource.Token); + authorizationTask.Wait(); + if (authorizationTask.IsCompleted) + { + userCredential = authorizationTask.Result; + token = userCredential?.Token; + } + } + + if (!IsValidToken(token)) + { + // The token is still invalid after all of our attempts to refresh it. The user did not complete the + // authorization flow, so we interpret that as a cancellation. + throw new BlogClientOperationCancelledException(); + } + + // Stash the valid user credentials. + tc.Token = userCredential; } public void OverrideOptions(IBlogClientOptions newClientOptions) @@ -145,15 +188,7 @@ namespace OpenLiveWriter.BlogClient.Clients public BlogInfo[] GetUsersBlogs() { var cancellationTokenSource = new CancellationTokenSource(); - var userCredentialsTask = GetOAuth2AuthorizationAsync(Credentials.Username, cancellationTokenSource.Token); - userCredentialsTask.Wait(cancellationTokenSource.Token); - - BloggerService service = new BloggerService(new BaseClientService.Initializer() - { - HttpClientInitializer = userCredentialsTask.Result - }); - - var listBlogsTask = service.Blogs.ListByUser("self").ExecuteAsync(); + var listBlogsTask = GetService().Blogs.ListByUser("self").ExecuteAsync(); listBlogsTask.Wait(cancellationTokenSource.Token); return listBlogsTask.Result?.Items?.Select(x => new BlogInfo(x.Id, x.Name, x.Url)).ToArray(); }