Search code examples
oauth-2.0google-oauthgoogle-api-dotnet-clientmailkitchilkat-email

Erratic gmail OAuth2 authentication with MailKit and Google APIs


The MailKit and Google API C# code below was written to authenticate gmail access tokens in my Win10 desktop app. However, the call to client.AuthenticateAsync usually, but not always, fails and indicates an incorrect user ID and password.

I've compared the client credentials during failed attempts with those during successful attempts and they are identical. I've also checked the gmail account I am using to see if it wants me to verify that it is indeed me that is attempting the authentication, but it says everything is okay. The same behavior occurs with a different set of credentials as well as with a different gmail email account.

I've also written a Chilkat Software version that doesn't use MailKit or the Google APIs. It uses exactly the same credentials and access tokens and it never fails, even when MailKit/Google API version is complaining about erroneous user ids and passwords. I'd like to use MailKit instead simply because it is free and Chilkat isn't.

I've had the same issues with the MailKit version regardless of which version of .NET I've tried (4.8.1, 6, and 8), but the Chilkat version always works with all of them. Any ideas/suggestions would be appreciated.

  static async System.Threading.Tasks.Task Main(string[] args)
  {
     string from = $"[email protected]";
     string path = @"ClientCredentials.json";
     string[] scopes = new[] { "https://mail.google.com/" };
     string accessToken = @"";

     try
     {
        var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
            GoogleClientSecrets.FromFile(path).Secrets,
            scopes,
            mailbox,
            CancellationToken.None,
            new FileDataStore(Directory.GetCurrentDirectory(), true));

        using (var client = new SmtpClient())
        {
           client.Connect("smtp.gmail.com", 465, true);

           var oauth2 = new SaslMechanismOAuth2(from, credential.Token.AccessToken);
           accessToken = credential.Token.AccessToken;
           await client.AuthenticateAsync(oauth2);

           if (!client.IsAuthenticated)
           {
              // Refresh the access token
              if (credential.Token.IsStale)
              {
                 await credential.RefreshTokenAsync(CancellationToken.None);
                 accessToken = credential.Token.AccessToken;

                 oauth2 = new SaslMechanismOAuth2(from, accessToken);
                 await client.AuthenticateAsync(oauth2);
              }
           }
           client.Disconnect(true);
        }
     }
     catch (Exception ex)
     {
        Console.WriteLine($"An error occurred: {ex.Message}\n{ex.StackTrace}");
     }
  }

Solution

  • I haven't tried debugging your code, but I can spot a few problems just looking at it:

    var oauth2 = new SaslMechanismOAuth2(from, credential.Token.AccessToken);
    accessToken = credential.Token.AccessToken;
    await client.AuthenticateAsync(oauth2);
    
    if (!client.IsAuthenticated)
    {
        // Refresh the access token
        if (credential.Token.IsStale)
        {
            await credential.RefreshTokenAsync(CancellationToken.None);
            accessToken = credential.Token.AccessToken;
    
            oauth2 = new SaslMechanismOAuth2(from, accessToken);
            await client.AuthenticateAsync(oauth2);
        }
    }
    

    The exception you are getting is on the above await client.AuthenticateAsync(oauth2);

    You then check if authentication was successful to see if you need to refresh your auth token, but control flow never reaches that if-statement because an AuthenticationException was thrown, so it never refreshes the token or tries to authenticate a second time.

    Change your code to this:

    // Refresh the access token if it is stale
    if (credential.Token.IsStale)
    {
        await credential.RefreshTokenAsync(CancellationToken.None);
    }
    
    var oauth2 = new SaslMechanismOAuth2(from, credential.Token.AccessToken);
    await client.AuthenticateAsync(oauth2);
    

    Now it'll probably work.