Search code examples
google-apijwtgoogle-oauthgmail-api

Authorize GMail API with JWT


I'm trying to send an email through the gmail API from a Node.js application. I had this working, following the documentation and using the node-mailer package. However, I noticed that when we change our organizations password, the connection is no longer good (which makes sense). I'm therefore trying to authorize with a JWT instead.

The JWT is correctly generated and posted to https://oauth2.googleapis.com/token. This request then returns an access_token.

When it comes time to write and send the email, I tried to simply adapt the code that was previously working (at the time with a client_secret, client_id and redirect_uris):

const gmail = google.gmail({ version: 'v1', auth: access_token });
  gmail.users.messages.send(
    {
      userId: 'email',
      resource: {
        raw: encodedMessage
      }
    },
    (err, result) => {
      if (err) {
        return console.log('NODEMAILER - The API returned: ' + err);
      }

      console.log(
        'NODEMAILER  Sending email reply from server: ' + result.data
      );
    }
  );

The API keeps returning Error: Login Required.

Does anyone know how to solve this?

EDIT

I've modified my code and autehntication to add the client_id and client_secret:

const oAuth2Client = new google.auth.OAuth2(
      credentials.gmail.client_id,
      credentials.gmail.client_secret,
      credentials.gmail.redirect_uris[0]
    );

    oAuth2Client.credentials = {
      access_token: access_token
    };
    const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
    gmail.users.messages.send(
      {
        userId: 'email',
        resource: {
          raw: encodedMessage
        }
      },
      (err, result) => {
        if (err) {
          return console.log('NODEMAILER - The API returned: ' + err);
        }

        console.log(
          'NODEMAILER  Sending email reply from server: ' + result.data
        );
      }
    );

But now the error is even less precise: Error: Bad Request


Solution

  • Here's the final authorization code that worked for me:

    var credentials = require('../../credentials');
        const privKey = credentials.gmail.priv_key.private_key;
    
        var jwtParams = {
          iss: credentials.gmail.priv_key.client_email,
          scope: 'https://www.googleapis.com/auth/gmail.send',
          aud: 'https://oauth2.googleapis.com/token',
          exp: Math.floor(new Date().getTime() / 1000 + 120),
          iat: Math.floor(new Date().getTime() / 1000),
          sub: [INSERT EMAIL THAT WILL BE SENDING (not the service email, the one that has granted delegated access to the service account)]
        };
    
        var gmail_token = jwt.sign(jwtParams, privKey, {
          algorithm: 'RS256'
        });
    
        var params = {
          grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
          assertion: gmail_token
        };
    
        var params_string = querystring.stringify(params);
    
        axios({
          method: 'post',
          url: 'https://oauth2.googleapis.com/token',
          data: params_string,
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        }).then(response => {
          let mail = new mailComposer({
            to: [ARRAY OF RECIPIENTS],
            text: [MESSAGE CONTENT],
            subject: subject,
            textEncoding: 'base64'
          });
    
          mail.compile().build((err, msg) => {
            if (err) {
              return console.log('Error compiling mail: ' + err);
            }
    
            const encodedMessage = Buffer.from(msg)
              .toString('base64')
              .replace(/\+/g, '-')
              .replace(/\//g, '_')
              .replace(/=+$/, '');
    
            sendMail(encodedMessage, response.data.access_token, credentials);
          });
        });
    

    So that code segment above uses a private key to create a JSON Web Token (JWT), where: iss is the service account to be used, scope is the endpoint of the gmail API being accessed (this must be preauthorized), aud is the google API oAuth2 endpoint, exp is the expiration time, iat is the time created and sub is the email the service account is acting for.

    The token is then signed and a POST request is made to the Google oAuth2 endpoint. On success, I use the mailComposer component of NodeMailer to build the email, with an array of recipients, a message, a subject and an encoding. That message is then encoded.

    And here's my sendMail() function:

    const oAuth2Client = new google.auth.OAuth2(
          credentials.gmail.client_id,
          credentials.gmail.client_secret,
          credentials.gmail.redirect_uris[0]
        );
    
        oAuth2Client.credentials = {
          access_token: access_token
        };
    
        const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
        gmail.users.messages.send(
          {
            userId: 'me',
            resource: {
              raw: encodedMessage
            }
          },
          (err, result) => {
            if (err) {
              return console.log('NODEMAILER - The API returned: ' + err);
            }
    
            console.log(
              'NODEMAILER  Sending email reply from server: ' + result.data
            );
          }
        );
    

    In this function, I am creating a new googleapis OAuth2 object using the credentials of the service account (here stored in an external file for added security). I then pass in the access_token (generated in the auth script with the JWT). The message is then sent.

    Pay attention to the userId: 'me' in the sendMail() function, this was critical for me.