Search code examples
rubyoauthgmailimapsasl

Generating SASL XOAUTH2 client response for Gmail IMAP access using Ruby


I'm trying to access my Gmail emails over IMAP using XOAUTH2 in Ruby.

I've successfully generated an access token (and refresh token) by authenticating using OAuth 2.0 with the oauth2 gem. I'm going to use gmail_xoauth to access Gmail over IMAP. So I now need to generate the SASL initial client response, as per the Gmail XOAuth2 docs:

The SASL XOAUTH2 initial client response has the following format:  

    base64("user=" {User} "^Aauth=Bearer " {Access Token} "^A^A")

using the base64 encoding mechanism defined in RFC 4648.
^A represents a Control+A (\001).

I'm not clear how I represent the "Control+A" in my string. Do I simply use ^A?

key = Base64.encode64("user=#{email}^Aauth=Bearer #{access_token_obj.token}^A^A")

This python script uses \1 in place of ^A. I've also tried \001. Whatever I try, when authenticating (in irb) with the result I get:

>> imap = Net::IMAP.new('imap.gmail.com', 993, usessl=true, certs=nil, verify=false)
>> imap.authenticate('XOAUTH2', email, key)
OpenSSL::SSL::SSLError: SSL_write:: bad write retry

That error could be entirely unrelated, but I'm not confident any option I've tried is correct.


Solution

  • Finally figured it out... I didn't need to do the Base64 encoding step at all!

    gmail_xoauth adds the XOAUTH authenticator to Net::IMAP itself. I realised that this only expects the unencoded access_token from Google, rather than the longer Base64-encoded string.

    So, if:

    email = `[email protected]`
    # The result of the OAuth2 dance (as well as a refresh_token):
    access_token = 'ya13.AHES6Y3F54_5fAoz_8VuG-7pzQAo3R0_ukt7dhfgRnJh41Q'
    

    then I don't have to Base64 encode anything. I just do:

    imap = Net::IMAP.new('imap.gmail.com', 993, usessl=true, certs=nil, verify=false)
    imap.authenticate('XOAUTH2', email, access_token)
    

    and I get back:

    #<struct Net::IMAP::TaggedResponse tag="RUBY0001", name="OK", data=#<struct Net::IMAP::ResponseText code=nil, text="[email protected] Fred Bloggs authenticated (Success)">, raw_data="RUBY0001 OK [email protected] Fred Bloggs authenticated (Success)\r\n">
    

    (As a bonus, this is a handy Ruby script for getting the access_token from the OAuth dance.)