Wednesday, March 16, 2016

Gmail Oauth2 with Spring and JavaMail with Grails

Setting up Gmail Email Sending with Grails (2+, 3+) Service

Most Web applications will send some sort of email to users. Lost passwords, invitations to join. Its a good way to reach professional users. Not so great for young people (they never check them) but in order to send emails, you have to get the connection right. I didn';t build a particular groovy way, I used java code within a grails service to make a simple method available to controllers and other serivces:

interface EmailService {
    /**     
    * Send a set of emails and collect the results     
    * @param addresses list of addresses to send to
    * @param subject subject of email    
    * @param text  body of email   
    * @return a list of message IDs returned from the sending service    
    */    
    def send(List<String> addresses, String subject, String text)

    /**     
    * Setup any needed state from config files etc.     
    */    
    void initialize()
}

Most of the default grails java mail setup questions (I'm using spring mail with grails because its mostly already on the classpath) you will find involve turning off enhanced seucirty in your gmail account in order to get around the error message you will recieve on startup:

Authentication failed; nested exception is javax.mail.AuthenticationFailedException: 534-5.7.14 <https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbvO
534-5.7.14 SC261Te39VZ5jtNBz2mvwNtIGtZLxYulCRb8D2u6rGTAg69U2-tQsPDzI1YPgWUbVo1ZQm
534-5.7.14 MlSxYEJHBzyTk-tQKy-6GN5HACShag4XcqNYlxbyWHYvMyMICSTPuwRFzM_Rn2kUOKLcoY
534-5.7.14 hcEmEC6i4DrvVh4h8KTTdK1VxgyDwD6QzfDxWgUa0vM7ZcLRfIURv1CThW4B0G5XvVgG3a
534-5.7.14 t-wJ_9P1uU8YoJK2c-QbrhZe2H9qo> Please log in via your web browser and
534-5.7.14 then try again.
534-5.7.14  Learn more at
534 5.7.14  https://support.google.com/mail/answer/78754 h24sm11972231ioi.17 - gsmtp

Someone just tried to sign in to your Google Account someone@gmailaddress.com from an app that doesn't meet modern security standards." - Oh great, here goes my day

This cryptic error results from using a setup like (in resources.groovy):

emailService(GmailEmailService) {
                template=ref("templateMessage")
                mailSender=ref("mailSender")
            }

templateMessage(SimpleMailMessage) {
                from="email@sendergmail.com"
            }

mailSender(JavaMailSenderImpl) {
                host="smtp.gmail.com"
                port=587
                protocol="smtp"
                username="email@sendergmail.com"
                password="yourpassword"
                javaMailProperties = [
                        "mail.transport.protocol" : "smtp",
                        "mail.smtp.auth" : true,
                        "mail.smtp.starttls.enable" : "true",
                        "mail.smtp.quitwait" : true,
                        "mail.debug" : true
                ]
            }

But what if you wanted to use enhanced security? Well available in the more recent versions of JavaMail is support of Oauth2.

To begin with though, you are going to need to follow the google guidelines here to recieve api credentials for your webserver. You should end up with a set of JSON credentials for your service account. :

{
  "type": "service_account",
  "project_id": "someid",
  "private_key_id": "SOMEID",
  "private_key": "YOURKEY",
  "client_email": "someemail",
  "client_id": "someid",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "someurl"
}

There is a branch here. You can choose to give the service account Delegate domain-wide authority (which I have done) or leave it at user-interactive mode. Since I dont want to have to ask my own company user account for permissions (which makes sense for a back-end webservice making sending email from only one user), I chose  to upgrade the account to domain-wide, which can be done in the console.

Adding Domain Wide Delegation means no prompts, but using an additional secret file

We will need to use these credentials to create oauth tokens that can be sent with each senmail request. To create these tokens (oauthtoken) you can use the Google Java API. (at the time of writing, the latest central maven available was: 'com.google.api-client:google-api-client:1.21.0') . Google has a guide available - but essentially you will be using the builder to setup a trusted account.

GoogleCredential credential = GoogleCredential.fromStream(new FileInputStream(authFile)).createScoped(Collections.singleton("https://www.googleapis.com/auth/gmail.send")) //for SMTP access only

Then email setup can be pretty easy. A lot of the boiler plate of creating a javax.mail.transport can be made easier by using some Java Code provided by Google itself.

If like us, you were using google apps accounts - there is additional information required. As you may get responses like:

DEBUG SMTP: SASL: no response
DEBUG SMTP: SASL authentication failed

Things to check:

* You have allowed the client id access to the same scope you are requesting (Google Scopes listing)
* You are creating your Google credential and access token with a user to impersonate:

GoogleCredential credential = GoogleCredential.fromStream(new FileInputStream(authFile))                .createScoped(Collections.singleton("https://www.googleapis.com/auth/gmail.send"))
credential.serviceAccountUser = "someuser@yourdomain.com" //important

Final Thoughts

After going through all of this trouble, I ended up switching to using the google api client and the gmail api client for java, while still using JavaMail for convience (google has a great guide here)

By the way, I reccomend using scheduling with retries in order to make sure your customers recieve their emails. It can be really annoying when an app fails to send that crucial forgot password email - they will often leave and not come back.

Turns out doing it the right way is a bit more involved - but worth it. One day your email's wont stop randomly working when google finally axes support for the basic authentication.