How to authenticate to Gmail with Rebex Mail Pack using OAuth 2.0
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
- Register your application for OAuth 2.0 authentication at Google. You'll get the Client ID and Client Secret constants that identify your app.
- Enable access to your Gmail mailbox using IMAP protocol. (No need to explicitly enable SMTP.)
- Add reference to Google.Apis.OAuth2.v2 library. It's available as NuGet package. To include it in your project, run following command in NuGet's Package Manager Console:
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.