5

OAuth2 is producing "Username and Password not accepted" error when try to send email with Gmail+ Nodejs+Nodemailer

Code - Nodejs - Nodemailer and xoauth2

var nodemailer = require("nodemailer");

var generator = require('xoauth2').createXOAuth2Generator({
    user: "", // Your gmail address.

    clientId: "",
    clientSecret: "",
    refreshToken: "",
});



// listen for token updates
// you probably want to store these to a db
generator.on('token', function(token){
    console.log('New token for %s: %s', token.user, token.accessToken);
});


// login
var smtpTransport = nodemailer.createTransport({
    service: 'gmail',
    auth: {
        xoauth2: generator
    }
});


var mailOptions = {
    to: "",
    subject: 'Hello ', // Subject line
    text: 'Hello world ', // plaintext body
    html: '<b>Hello world </b>' // html body
};


smtpTransport.sendMail(mailOptions, function(error, info) {
  if (error) {
    console.log(error);
  } else {
    console.log('Message sent: ' + info.response);
  }
  smtpTransport.close();
});

issues:

  • I used Google OAuth2 playground to create the tokens, https://developers.google.com/oauthplayground/

  • It looks to grab a valid accessToken ok, using the refreshToken, (i.e. it prints the new access token on the screen.) No errors until it tries to send the email.

  • I added the optional accessToken: but got the same error. ( "Username and Password not accepted")

  • I am not 100% sure about the "username", the docs say it needs a "user" email address - I guess the email of the account that created to token, but is not 100% clear. I have tried several things and none worked.

  • I have searched the options on the gmail accounts, did not find anything that looks wrong.

  • Also, when I did this with Java, it needed the google userID rather than the email address, not sure why this is using the email address and the Java is using the UserId.

eddyparkinson
  • 3,680
  • 4
  • 26
  • 52

3 Answers3

12

nodemailer fails with a "compose" scope

The problem was the "scope"

it fails with: https://www.googleapis.com/auth/gmail.compose

but works ok if I use https://mail.google.com/

eddyparkinson
  • 3,680
  • 4
  • 26
  • 52
  • I am facing a similar issue even through I am using the correct scope. Are you aware of any possible issue that could cause that? (I explained my problem here: http://stackoverflow.com/questions/19766912/how-do-i-authorise-an-app-web-or-installed-without-user-intervention-canonic) – Abdelrahman Shoman May 18 '17 at 13:40
  • @Abdel-RahmanShoman I left an issue on 'Nodemailer' on github - They said scope, which fixed the problem for me. – eddyparkinson May 29 '17 at 02:27
  • what scope we should use? @eddyparkinson – ngLover Nov 27 '18 at 10:32
  • @ngLover see scope values - Gmail API v1 - here https://developers.google.com/oauthplayground/ – eddyparkinson Nov 30 '18 at 05:07
  • same happened when I was using just the send scope (https://www.googleapis.com/auth/gmail.send) used the recommended scope and all went well – niCad Dec 28 '18 at 08:43
  • I used this [tutorial](https://medium.com/@nickroach_50526/sending-emails-with-node-js-using-smtp-gmail-and-oauth2-316fe9c790a1) and it suggested scope as only gmail.compose. But this did not worked with nodemailer. With https://mail.google.com/ it works. – ivosh May 29 '19 at 10:09
  • Chiming in here in 2020 and this is still an issue. Indeed, the full scope is required. This _does_ seem like an error in the nodemailer implementation - if we are only sending email with it, we should only need the 'send' scope, not the full https://mail.google.com/ scope. – fullStackChris Aug 10 '20 at 09:24
1

Simply just do the following:

1- Get credentials.json file from here https://developers.google.com/gmail/api/quickstart/nodejs press enable the Gmail API and then choose Desktop app

2- Save this file somewhere along with your credentials file

const fs = require('fs');
const readline = require('readline');
const {google} = require('googleapis');

// If modifying these scopes, delete token.json.
const SCOPES = ['https://mail.google.com'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = 'token.json';

// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
    if(err){
        return console.log('Error loading client secret file:', err);
    }

    // Authorize the client with credentials, then call the Gmail API.
    authorize(JSON.parse(content), getAuth);
});

/**
 * Create an OAuth2 client with the given credentials, and then execute the
 * given callback function.
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback to call with the authorized client.
 */
function authorize(credentials, callback) {
    const {client_secret, client_id, redirect_uris} = credentials.installed;
    const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);

    // Check if we have previously stored a token.
    fs.readFile(TOKEN_PATH, (err, token) => {
        if(err){
            return getNewToken(oAuth2Client, callback);
        }
        oAuth2Client.setCredentials(JSON.parse(token));
        callback(oAuth2Client);
    });
}

/**
 * Get and store new token after prompting for user authorization, and then
 * execute the given callback with the authorized OAuth2 client.
 * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
 * @param {getEventsCallback} callback The callback for the authorized client.
 */

function getNewToken(oAuth2Client, callback) {
    const authUrl = oAuth2Client.generateAuthUrl({
        access_type: 'offline',
        scope: SCOPES,
    });
    console.log('Authorize this app by visiting this url:', authUrl);
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });
    rl.question('Enter the code from that page here: ', (code) => {
        rl.close();
        oAuth2Client.getToken(code, (err, token) => {
        if (err) return console.error('Error retrieving access token', err);
        oAuth2Client.setCredentials(token);
        // Store the token to disk for later program executions
        fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
            if (err) return console.error(err);
            console.log('Token stored to', TOKEN_PATH);
        });
        callback(oAuth2Client);
        });
    });
}

function getAuth(auth){

}

3 - Run this file by typing in your terminal: node THIS_FILE.js

4- You'll have token.json file

5- take user information from credentials.json and token.json and fill them in the following function

const nodemailer = require('nodemailer');
        const { google } = require("googleapis");
        const OAuth2 = google.auth.OAuth2;

        const email = 'gmail email'
        const clientId = ''
        const clientSecret = ''
        const refresh = ''



        const oauth2Client = new OAuth2(
            clientId,
            clientSecret,
        );

        oauth2Client.setCredentials({
            refresh_token: refresh
        });
        const newAccessToken = oauth2Client.getAccessToken()


        let transporter = nodemailer.createTransport(
            {
                service: 'Gmail',
                auth: {
                    type: 'OAuth2',
                    user: email,
                    clientId: clientId,
                    clientSecret: clientSecret,
                    refreshToken: refresh,
                    accessToken: newAccessToken
                }
            },
            {
                // default message fields

                // sender info
                from: 'Firstname Lastname <your gmail email>'
            }
        );

        const mailOptions = {
            from: email,
            to: "",
            subject: "Node.js Email with Secure OAuth",
            generateTextFromHTML: true,
            html: "<b>test</b>"
        };

        transporter.sendMail(mailOptions, (error, response) => {
            error ? console.log(error) : console.log(response);
            transporter.close();
        });
Adam
  • 6,447
  • 1
  • 12
  • 23
0

If your problem is the scopes, here is some help to fix

Tried to add this as an edit to the top answer but it was rejected, don't really know why this is off topic?

See the note here: https://nodemailer.com/smtp/oauth2/#troubleshooting

How to modify the scopes

The scopes are baked into the authorization step when you get your first refresh_token. If you are generating your refresh token via code (for example using the Node.js sample) then the revised scope needs to be set when you request your authUrl.

For the Node.js sample you need to modify SCOPES:

// If modifying these scopes, delete token.json.
-const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
+const SCOPES = ['https://mail.google.com'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.

And then the call to oAuth2Client.generateAuthUrl will produce a url that will request authorization from the user to accept full access.

from the Node.js sample:

function getNewToken(oAuth2Client, callback) {
  const authUrl = oAuth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES,
  });
BruceJo
  • 591
  • 6
  • 17