How to authenticate to Gmail with Rebex Mail Pack using OAuth 2.0

  |   Lukas Matyska

Google Account passwords are precious and it's not a good idea to share them with random third-party applications you don't fully trust. That's why Google has been promoting OAuth 2.0, an open authentication protocol that makes it possible for users to grant access to Gmail and other services to third-party applications without having to reveal their password to them.

For the same reason, username/password authentication is disabled by default when accessing Gmail mailbox using IMAP, POP3 or SMTP protocols. Following errors occur when authenticating with classic IMAP/POP3/SMTP clients:

Please log in via your web browser: https://support.google.com/mail/accounts/answer/78754 (Failure) (NO).  
[AUTH] Web login required: https://support.google.com/mail/answer/78754
https://support.google.com/mail/answer/78754 bh1sm1307712wjb.53 - gsmtp (534).  

Although you can still allow username/password access by enabling Less secure apps in your Google Account settings, upgrading your application to use OAuth 2.0 authentication is often a better choice. The rest of this post will guide you through that process.

We also have a sample project that demonstrates the whole process.

1. Getting started

    PM>  Install-Package Google.Apis.OAuth2.v2

2. Get OAuth 2.0 access token from Google

To authenticate to Gmail using OAuth 2.0, your application needs to obtain an access token first. This can be done using Google.Apis.OAuth2.v2 package and its GoogleWebAuthorizationBroker.AuthorizeAsync() method. This may launch a browser window to make it possible for the user to authenticate to Google without revealing his password to the application.

// used namespaces
using System.IO;
using System.Threading;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Util.Store;

// client ID and client secret, which identifies your application
// see http://blog.rebex.net/howto-register-gmail-oauth
const string ClientId = @"YOUR-CLIENT-ID-HERE";
const string ClientSecret = @"YOUR-CLIENT-SECRET-HERE";

// path to local token storage
const string TokenStoragePath = @"c:\data\user-tokens";

// scope definition for accessing Gmail and getting email info
static readonly string[] GmailScope = { "https://mail.google.com", "email" };

/// <summary>
/// Obtains Google's OAuth 2.0 access token identified by the specified local ID.
/// </summary>
/// <param name="localId">A local ID, which identifies an access token in local storage.
/// If the token is found in the local storage, the user is not redirected to Google's login page.
/// Local ID can be any string e.g. username.</param>
/// <returns>The Google's OAuth 2.0 access token.</returns>
static string GetAccessToken(string localId)
{
    // prepare client secrets
    var secrets = new ClientSecrets();
    secrets.ClientId = ClientId;
    secrets.ClientSecret = ClientSecret;

    // get Google's user credential
    UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
        secrets, // client secrets
        GmailScope, // scope to access Gmail
        localId, // token ID
        CancellationToken.None, // no cancellation
        new FileDataStore(TokenStoragePath, true) // file storage for tokens
    ).Result;

    // refresh token if necessary
    RefreshToken(credential, false);

    // return access token
    return credential.Token.AccessToken;
}

/// <summary>
/// Refreshes the Google's OAuth 2.0 token.
/// </summary>
/// <param name="credential">User credential holding the token.</param>
/// <param name="force">True to force refreshing; false to refresh only if token is expired.</param>
static void RefreshToken(UserCredential credential, bool force)
{
    // check whether to refresh the token
    if (force || credential.Token.IsExpired(Google.Apis.Util.SystemClock.Default))
    {
        bool succes;
        Google.GoogleApiException error = null;
        try
        {
            // refresh token
            succes = credential.RefreshTokenAsync(CancellationToken.None).Result;
        }
        catch (Google.GoogleApiException ex)
        {
            succes = false;
            error = ex;
        }
        if (!succes)
            throw new InvalidOperationException("Refreshing OAuth 2.0 token failed.", error);
    }
}

3. Get e-mail address associated with the access token

To authenticate to Gmail with IMAP or SMTP using OAuth 2.0, you also need the account's e-mail address in addition to the access token (obtained by the code above). Fortunately, the Google.Apis.OAuth2.v2 package can retrieve the address associated with a token using the Oauth2Service.Tokeninfo() method:

// used namespaces
using Google.Apis.Oauth2.v2;
using Google.Apis.Services;

/// <summary>
/// Gets the email address associated with Google's OAuth 2.0 token.
/// </summary>
/// <param name="accessToken">Google's OAuth 2.0 access token.</param>
/// <returns>The email address associated with Google's OAuth 2.0 token.</returns>
static string GetEmail(string accessToken)
{
    try
    {
        // request token info
        var service = new Oauth2Service(new BaseClientService.Initializer());
        var req = service.Tokeninfo();
        req.AccessToken = accessToken;
        var tokeninfo = req.Execute();

        // return email associated with given token
        return tokeninfo.Email;
    }
    catch (Google.GoogleApiException ex)
    {
        throw new InvalidOperationException("Extracting email address from OAuth 2.0 token failed.", ex);
    }
}

4. Construct initial XOAUTH2 client response

Now, the application has everything it needs. But before it actually establishes an IMAP or SMTP session, it has to construct an initial client response (used instead of username/password credentials) from the access token and e-mail address:

// used namespaces
using System.Text;

/// <summary>
/// Gets the initial client response in SASL XOAUTH2 format.
/// </summary>
/// <param name="email">The email address associated with Google's OAuth 2.0 token.</param>
/// <param name="accessToken">Google's OAuth 2.0 access token.</param>
/// <returns>The initial client response in SASL XOAUTH2 format.</returns>
static string PrepareInitialResponse(string email, string accessToken)
{
    // SASL XOAUTH2 initial client response format:
    //   base64("user=" {email} "^A" "auth=Bearer " {access token} "^A^A")

    // prepare XOAUTH2 initial client response
    string raw = string.Format("user={0}{1}auth=Bearer {2}{1}{1}", email, '\x1', accessToken);

    return Convert.ToBase64String(Encoding.ASCII.GetBytes(raw));
}

5. Establish IMAP or SMTP session

Now, the app can use Rebex Mail Pack to connect to Gmail using IMAP or SMTP protocol and authenticate using the XOAUTH2 initial client response it constructed from the OAuth 2.0 token:

// used namespaces
using Rebex.Net;

static void Main(string[] args)
{
    // local ID used to identify the user locally
    // (does not need to correspond to Google's user name)
    string localId = "user123";

    // get Google's OAuth 2.0 access token
    string accessToken = GetAccessToken(localId);

    // get email associated with the token
    string email = GetEmail(accessToken);

    // prepare XOAUTH2 initial client response
    string initialResponse = PrepareInitialResponse(email, accessToken);

    // connect and log in to Gmail using OAuth 2.0
    var client = new Imap();
    client.Connect("imap.gmail.com", SslMode.Implicit);
    client.Login(initialResponse, ImapAuthentication.OAuth20);

    // do something...
    Console.WriteLine("Successfully logged into {0} mailbox.", email);
}

The application can access the Gmail mailbox now. If you are new to Rebex Mail Pack, see the list of Rebex Mail Pack features to see what's available.