Google shuts down "Less secure app" feature on their mail services last month. Because of this, I cannot just use Nodemailer by using email and password only. I am forced to setup Nodemailer with Google OAuth. But, the refresh token expires every week. What should I do?
Here's my transporter code.
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
type: 'OAuth2',
user: process.env.GA_MAIL,
accessToken,
clientId: process.env.GA_CLIENT_ID,
clientSecret: process.env.GA_CLIENT_SECRET,
refreshToken: process.env.GA_REFRESH_TOKEN,
},
});
This is the problem better worded :
This is the file token.js :
const { google } = require('googleapis');
const path = require('path');
const fs = require('fs');
const credentials = require('./email.json');
// Replace with the code you received from Google
const code = "";
const { client_secret, client_id, redirect_uris } = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
oAuth2Client.getToken(code).then(({ tokens }) => {
const tokenPath = path.join(__dirname, 'token.json');
fs.writeFileSync(tokenPath, JSON.stringify(tokens));
console.log('Access token and refresh token stored to token.json');
});
when running the token.js file a file called token.json gets created with some access tokens :
{"access_token":"","refresh_token":"","scope":"https://www.googleapis.com/auth/gmail.send","token_type":"Bearer","expiry_date":1656595108942}
but after 7 days the access_token expires.
A solution I came up with , though it's untested , is to have inside a .catch() block a call for a token refresh like so :
main.js :
//...
const refreshToken = require("./refresh_token.js");
//...
}).catch((err) => {
reply(interaction,"Error, Refreshing token.. Please try again");
console.error(err);
refreshToken();
return;
});
//...
refresh_token.js :
const { google } = require('googleapis');
const path = require('path');
const fs = require('fs');
const credentials = require('./email.json');
const token = require("./token.json");
module.exports = () => {
const code = token.refresh_token;
const { client_secret, client_id, redirect_uris } = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
oAuth2Client.getToken(code).then(({ tokens }) => {
const tokenPath = path.join(__dirname, 'token.json');
fs.writeFileSync(tokenPath, JSON.stringify(tokens));
console.log('Access token and refresh token stored to token.json');
});
}
The reason I put it in a catch statement is because when the token is not expired we get a invalid_grant error when trying to run oAuth2Client.getToken(token.refresh_token)
^^ THIS HAS NOT BEEN TESTED ^^
if the refresh_token expires as well then I have no idea how to do this , please help
Setting the testing mode into a production mode in Google Developer Console will prevent refresh token from exipiring.
Related
My backend generates the access_token with below redirect controller:
const logged = async (req: Request, res: Response) => {
const code = (req.query.code as string) || null;
const state = (req.query.state as string) || null;
const error = (req.query.error as string) || null;
const oauth2Client = new youtube.auth.OAuth2(
GOOGLE_CLIENT_ID,
GOOGLE_SECRET,
GOOGLE_REDIRECT_URI,
);
const { tokens: { access_token }} = await oauth2Client.getToken(code);
res.send({ access_token });
};
Returned code is similar to: "ya29.<MUCH_MORE_CHARACTERS_HERE>_BtA0163"
Then somewhere else I'm trying to create a client with this access token like this:
const youtube = require("#googleapis/youtube");
const youtubeApi = youtube.youtube({
version: "v3",
auth: "ya29.<MUCH_MORE_CHARACTERS_HERE>_BtA0163",
});
(Btw I'm aware that I could get the client on the redirect controller. I just don't want to)
But everytime I try to access something, it gives me error:
code: 400,
errors: [
{
message: 'API key not valid. Please pass a valid API key.',
domain: 'global',
reason: 'badRequest'
}
]
I believe your goal is as follows.
You want to use your access token to the client with googleapis for Node.js.
In your script, how about the following modification?
From:
const youtube = require("#googleapis/youtube");
const youtubeApi = youtube.youtube({
version: "v3",
auth: "ya29.<MUCH_MORE_CHARACTERS_HERE>_BtA0163",
});
To:
const { google } = require("googleapis");
const auth = new google.auth.OAuth2();
auth.setCredentials({ access_token: "ya29.<MUCH_MORE_CHARACTERS_HERE>_BtA0163" });
const youtubeApi = google.youtube({ version: "v3", auth });
When I send mail then, it successfully sends the mail. But after a few days, it says 'invalid_grant'. Then when I again generate a new refresh token from https://developers.google.com/oauthplayground and use it, it works again. So what's the problem that access token doesn't work after few days.
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET
const GOOGLE_REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI
const GOOGLE_REFRESH_TOKEN = process.env.GOOGLE_REFRESH_TOKEN
const oAuth2Client = new google.auth.OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI)
oAuth2Client.setCredentials({refresh_token: GOOGLE_REFRESH_TOKEN})
export async function sendMail(payload: IMailDTO): Promise<SMTPTransport.SentMessageInfo>{
const data = MailDTO.receiver(payload)
const accessToken = await oAuth2Client.getAccessToken()
const transport = nodemailer.createTransport({
// #ts-ignore
service: 'gmail',
auth: {
type: 'OAUTH2',
user: 'user#gmail.com',
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
refreshToken: GOOGLE_REFRESH_TOKEN,
accessToken,
}
})
const mailOptions = {
from: 'no-reply <no-reply#gmail.com>',
to: data.to,
subject: data.subject,
text: data.text,
html: data.html
}
let result = await transport.sendMail(mailOptions)
if(!result){
throw new Error("Email not sent. Try again.")
}
return result
}
The issue is not with your access token the issue is with your refresh token.
The first is the OAuth Playground will automatically revoke refresh tokens after 24h. You can avoid this by specifying your own application OAuth credentials using the Configuration panel.
The second being that if you have supplied your own OAuth Credentials and your application is still in testing then it will expire after seven days.
A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of "Testing" is issued a refresh token expiring in 7 days.
To have a refresh token that will not expire you need to create it using your own credentials and make sure to set your application in production.
I need to call a Dialogflow V2 APIs, but to do this i need to generate a token.
I found many codes about how to do this, but doesn't work for me.
I'm doing an API to generate token and then pass the token to another API to call DialogFlow APIs.
Can anyone help me how to generate a token to call Dialogflow V2 APIs?
My API to generate token Code below:
const express = require('express');
const router = express.Router();
const googleAuth = require('google-oauth-jwt');
function generateAccessToken() {
return new Promise((resolve) => {
googleAuth.authenticate(
{
email: "<myemail>",
key: "<myprivatekey>",
scopes: "https://www.googleapis.com/auth2/v4/token",
},
(err, token) => {
resolve(token);
},
);
});
}
router.get('/token', async(req, res) => {
try {
let tok = await generateAccessToken();
return res.status(200).send({ Token: tok});
} catch(err) {
return res.status(500).send({ error: 'Erro ao gerar token'});
}
});
module.exports = app => app.use('/', router);
Dialogflow V2 no longer uses developer/client access tokens. You need to use your service account key file to access the API. You can follow this article to set up your service account. Once you have set up your service account, you can check the sample implementation of dialogflow using nodejs.
Install client library:
npm install #google-cloud/dialogflow
Code snippet from the github link:
const dialogflow = require('#google-cloud/dialogflow');
const uuid = require('uuid');
/**
* Send a query to the dialogflow agent, and return the query result.
* #param {string} projectId The project to be used
*/
async function runSample(projectId = 'your-project-id') {
// A unique identifier for the given session
const sessionId = uuid.v4();
// Create a new session
const sessionClient = new dialogflow.SessionsClient();
const sessionPath = sessionClient.projectAgentSessionPath(projectId, sessionId);
// The text query request.
const request = {
session: sessionPath,
queryInput: {
text: {
// The query to send to the dialogflow agent
text: 'hello',
// The language used by the client (en-US)
languageCode: 'en-US',
},
},
};
// Send request and log result
const responses = await sessionClient.detectIntent(request);
console.log('Detected intent');
const result = responses[0].queryResult;
console.log(` Query: ${result.queryText}`);
console.log(` Response: ${result.fulfillmentText}`);
if (result.intent) {
console.log(` Intent: ${result.intent.displayName}`);
} else {
console.log(` No intent matched.`);
}
}
Below the code worked for me returning Bearer Token:
const express = require('express');
const router = express.Router();
const googleAuth = require('google-oauth-jwt');
const {google} = require('googleapis');
const request = require('request');
async function generateAccessToken2() {
const jwtClient = new google.auth.JWT(
"<email serviceaccount>",
null,
"<Private Key>",["https://www.googleapis.com/auth/indexing","https://www.googleapis.com/auth/cloud-platform","https://www.googleapis.com/auth/dialogflow"],
null
);
let tok = "";
tok = jwtClient.authorize();
return tok;
};
I'm getting the following error while trying to add a sign up with apple in my nodejs backend:
status: auth/missing-or-invalid-nonce
message: Duplicate credential received. Please try again with a new credential
Here is the code:
const clientId = 'com.mybusiness';
const { nonce: nonce } = await appleSignin.verifyIdToken(
appleIdentityToken,
{
audience: clientId,
ignoreExpiration: true,
}
);
const hashedNonce: string = crypto
.createHash('sha256')
.update(nonce)
.digest()
.toString();
const provider = new firebase.auth.OAuthProvider('apple.com');
const credential: OAuthCredential = provider.credential({
idToken: appleIdentityToken,
rawNonce: hashedNonce,
});
const userCredential: UserCredential = await this.clientAuth.signInWithCredential(
credential
);
I have created multiple website using this tutorial https://medium.com/#orels1/using-discord-oauth2-a-simple-guide-and-an-example-nodejs-app-71a9e032770 and they all worked perfectly until yesterday. The problem is that when a user authenticates and goes to https://mywebsite.com/api/discord/callback?code={discord response code} the api gets an error when trying to convert the code with a access_token.
This is the code for my discord api callback:
const router = express.Router();
const fetch = require('node-fetch');
const FormData = require('form-data');
const btoa = require('btoa');
const { catchAsync } = require(__dirname+'/utils');
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const redirect = encodeURIComponent('https://mywebsite.com/api/discord/callback');
router.get('/login', (req, res) => {
res.redirect(`https://discordapp.com/api/oauth2/authorize?client_id=${CLIENT_ID}&scope=identify&response_type=code&redirect_uri=${redirect}`);
});
router.get('/callback', catchAsync(async (req, res) => {
if (!req.query.code) throw new Error('NoCodeProvided');
const code = req.query.code;
const creds = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const response = await fetch(`https://discord.com/api/oauth2/token?grant_type=authorization_code&code=${code}&redirect_uri=${redirect}`,
{
method: 'POST',
headers: {
Authorization: `Basic ${creds}`,
},
});
const json = await response.json();
console.log(json)
res.redirect(`/finallogin/?token=${json.access_token}`);
}));
module.exports = router;
When a user authenticates, he gets redirected to https://mywebsite.com/finallogin/?token=undefined and I'm getting this error on console:
error: 'invalid_request',
error_description: 'Missing "code" in request.'
}
This was working perfectly before and just randomly broke a few days ago on all of my websites. Any help / solution / idea will be appreciated.
(Note: the "https://mywebsite.com" is just an example and not my actual website url)
Thanks,
RealPower2Maps.