I'm building a SaaS application which require read-access to a user's google calendar. After the user gives consent to access their calendar during the first sign-in, I want the application to be able to authorize itself to access any of the user's calendars at any time without having to prompt the user for authorization again.
Currently I'm trying to create an authentication flow following the '#react-oauth/google' node library (specifically the "authorization code flow" here: https://react-oauth.vercel.app/). In my frontend, I get a code from the user, which is sent and successfully received by my backend. The backend is then supposed to use the code to get an access token and a refresh token for that user, but the request to exchange the code for the access token (oAuth2Client.getToken(req.body.code);) is failing with error 401 (unauthorized client.)
My ultimate goal is to store the access token and refresh token in a database somewhere so that I can access that user's calendar later at any time.
If I treat the backend as an Oath Client on google cloud and pass in the credentials for that, I get error 401 - unauthorized client, but I've given it access to the calendar api on google console, as you can see in the image:
How can I resolve the issue that I'm facing?
I started reading about service accounts that can do this for you but I'm unsure how to proceed. I saw that they can do domain wide delegation but my users will be signing in from their personal gmail accounts so that option is not applicable for me.
Frontend Code:
import { useGoogleLogin } from '#react-oauth/google';
import axios from 'axios';
export const LoginModal = () => {
const googleLogin = useGoogleLogin({
flow: "auth-code",
onSuccess: async codeResponse => {
console.log(codeResponse);
const tokens = await axios.post("http://localhost:3001/auth/google/", {
code: codeResponse.code
});
console.log(tokens);
}
})
return (<>
...some html code
<button onClick={() => { googleLogin() }}>
..some more html code
</>)
}
Backend Code:
require('dotenv').config();
const express = require('express');
const {
OAuth2Client,
} = require('google-auth-library');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
const CLIENT_ID = "xxx";
const CLIENT_SECRET = "xxx";
// initialize oathclient
const oAuth2Client = new OAuth2Client(
CLIENT_ID,
CLIENT_SECRET,
'postmessage',
);
// get token from code given from frontend
app.post('/auth/google', async (req, res) => {
console.log(req.body.code)
const { tokens } = await oAuth2Client.getToken(req.body.code); // exchange code for tokens
res.json(tokens);
});
app.post('/auth/google/refresh-token', async (req, res) => {
const user = new UserRefreshClient(
CLIENT_ID,
CLIENT_SECRET,
req.body.refreshToken,
);
const { credentials } = await user.refreshAccessToken(); // obtain new tokens
res.json(credentials);
})
app.listen(3001, () => {
console.log(`server is running`)
});
I figured it out. Basically, I was putting in the wrong clientid/client secret in my google console because I thought the frontend and backend needed different oauth client IDs. I used the same oauth client secret/id for frontend and backend and made sure to follow the answers here:
https://github.com/MomenSherif/react-oauth/issues/12
MAKE SURE TO PUT "postmessage" AS YOUR REDIRECT_URI! It will not work without that.
Working code:
frontend is the same
backend:
require('dotenv').config();
const express = require('express');
const {
OAuth2Client,
} = require('google-auth-library');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
const CLIENT_ID = XXX
const CLIENT_SECRET = XXX
// initialize oathclient
const oAuth2Client = new OAuth2Client(
CLIENT_ID,
CLIENT_SECRET,
'postmessage',
);
// get token from code given from frontend
app.post('/auth/google', async (req, res) => {
console.log("got request!")
console.log(req.body.code)
const { tokens } = await oAuth2Client.getToken(req.body.code); // exchange code for token
res.json(tokens);
});
app.listen(3001, () => {
console.log(`server is running`)
});
Related
I'm having trouble getting the token once the user has logged in. I am supposed to get the token that is generated when I login through req.header so that when I ask for a route in the api (thunder api client) it should be able to give me access
My auth middleware:
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const token = req.headers['auth-token']
console.log(token)
res.json({
token: token
})
}
My routes:
const { Router } = require('express');
const router = Router();
const {
getAllUsers,
signupUser,
signinUser
} = require('../controllers/user.controller');
const auth = require('../middleware/auth');
router.get('/users', auth, getAllUsers);
router.post('/users/signup', signupUser);
router.post('/users/signin', signinUser);
module.exports = router;
when I user users/signin the response header:
Well well well, Apparently my problem was the following:
When I consumed by (thunder client) the route /users/signin -> it returned the object with the token. What I don't realize is that when I request the route that I want to be restricted to logged in users (only with the token) I must pass the same token that I get back from the signin in the headers of the new request (XD !) as seen in the photo.
I'm very new to web development, and am trying to implement authorization through a 3rd party for my app. Since I'm testing, the app as well as the server are both hosted locally. I have Oauth2.0 set up to authorize with github so that when I access the server (port 2), it asks for github auth then redirects to the app on port 1 with a code (the token is still null). However, if I go directly to the app hosted on port 1, I do not need to authenticate and there is no code given. Ideally, when the user goes to the app URL, they are redirected to github authentication, then back to the app.
I do not need to use the github API, I only want to use the github account login to authenticate and access the app.
API/server: localhost:3002
App: localhost:3001
const clientId = '...'
const clientSecret = '...'
const redirectUri = 'localhost:3001'
app.get('/', (req, res) => {
console.log('authorizing client through github...')
res.redirect(`https://github.com/login/oauth/authorize?client_id=${clientId}`);
});
...
const axios = require('axios');
let token = null;
app.get('/oauth-callback', (req, res) => {
const body = {
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
code: req.query.code
};
const opts = { headers: { accept: 'application/json' } };
axios.post(`https://github.com/login/oauth/access_token`, body, opts).
then(res => res.data['access_token']).
then(_token => {
console.log('My token:', token);
token = _token;
res.json({ ok: 1 });
}).
catch(err => res.status(500).json({ message: err.message }));
});
Apologies if this is a simple question, the Oauth docs aren't very helpful.
I'm trying to use the Spotify API and following their instructions on authorization found here: https://github.com/spotify/web-api-auth-examples/blob/master/authorization_code/app.js.
Their version of the Authorization code directly uses routes in the server code, but I wanted to separate the Authorization into its own route. Here is my version of the code:
const authRouter = require("express").Router();
const config = require("../utils/config");
const request = require("request");
const querystring = require("querystring");
// Spotify client configurations
const client_id = config.CLIENT_ID;
const client_secret = config.CLIENT_SECRET;
const redirect_uri = config.REDIRECT_URI;
const stateKey = "spotify_auth_state";
//...
// #route GET /
// #desc Prompt User to Login into Spotify
authRouter.get("/", async (req, res) => {
try {
var state = generateRandomString(16);
res.cookie(stateKey, state);
// Request for user full name, profile image, and email address.
var scope = "user-read-private user-read-email";
// 1. Get the user's authorization to access data.
res.redirect(
"https://accounts.spotify.com/authorize?" +
querystring.stringify({
response_type: "code",
client_id: client_id,
scope: scope,
redirect_uri: redirect_uri,
state: state,
})
);
} catch {
console.log("error.");
}
});
// #route GET /callback
// #desc Spotify callback to request access and refresh tokens
authRouter.get("/callback", async (req, res) => {
try {
var code = req.query.code || null; // The authorization code returned by the first call.
var state = req.query.state || null;
var storedState = req.cookies ? req.cookies[stateKey] : null;
// Check the state parameter
if (state === null || state !== storedState) {
res.redirect(
"/#" +
querystring.stringify({
error: "state_mismatch",
})
);
} else {
res.clearCookie(stateKey);
const authOptions = getAuthOptions(
code,
redirect_uri,
client_id,
client_secret
);
// 2. Request an access token and refresh token
request.post(authOptions, function (error, response, body) {
if (!error && response.statusCode === 200) {
// Authorize successful. Access and Refresh Tokens granted.
var access_token = body.access_token,
refresh_token = body.refresh_token;
// Send the tokens to the client so the client can use them in requests to the Spotify API.
res.redirect(
"/#" +
querystring.stringify({
access_token: access_token,
refresh_token: refresh_token,
})
);
} else {
res.redirect(
"/#" +
querystring.stringify({
error: "invalid_token",
})
);
}
});
}
} catch {
console.log("error.");
}
});
module.exports = authRouter;
And in my app.js:
const express = require("express");
const authRouter = require("./controllers/auth");
const cors = require("cors");
var cookieParser = require("cookie-parser");
// initialize app with Express to create client to communicate with Spotify
const app = express();
app.use(cors());
app.use(cookieParser());
app.use("/", authRouter);
module.exports = app;
Now when I start my server, my browser returns: "accounts.spotify.com redirected you too many times.". When I tried starting my server in incognito mode, the Spotify login prompt appears. After I enter my credentials, it returns: "accounts.spotify.com redirected you too many times."
I've tried clearing my cookies and caches but that does not work.
In addition, I've confirmed my redirect URI for my server is the same as my redirect URI in my Spotify application's settings.
What can be the reasons the auth seems to be stuck in an infinite loop?
What's causing the infinite loop is where the code sends the access and refresh tokens back to the client:
// Send the tokens to the client so the client can use them in requests to the Spotify API.
res.redirect(
"/#" +
querystring.stringify({
access_token: access_token,
refresh_token: refresh_token,
})
);
Since I have defined the following route:
authRouter.get("/", async (req, res) => {
The access and refresh tokens are redirected to the login page, which will then lead to the callback which redirects to the login again, creating an infinite loop.
How I solved this was to redirect the access and refresh tokens to a different component, not just to "/#" + query.string... as coded in Spotify's example code.
Spotify's example code does not lead to an infinite loop since they defined a /login route for the login page, but I opted my website's root to be the login page since in my case, authenticating should be the first step.
I think I'm missing something very simple here. I have a simple, one page node.js app that uses nodemailer to email any form data to my inbox.
My index.js file:
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var nodemailer = require('nodemailer');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'myemail#gmail.com',
pass: 'mypassword'
}
});
app.use(express.static('public')); //public folder with my html files
app.get('', function (req, res) {
res.sendFile(__dirname + "/");
})
app.post('/', function (req, res) {
response = {
name: req.body.name,
email: req.body.email,
message: req.body.message
};
var mailClient = {
from: 'myemail#gmail.com',
to: 'myemail#gmail.com',
subject: `Message from ${response.name}`,
text: 'MyMessage'
};
transporter.sendMail(mailClient, function (error, info) {
if (error) {
console.log(error); //not happening
} else {
res.redirect("/success.html"); //also not happening
}
});
})
var server = app.listen(80, function () {
var host = server.address().address
var port = server.address().port
console.log("App listening at http://%s:%s", host, port)
})
When I run this on my local machine using npm start in the root directory, the app runs perfectly fine on localhost. Nodemailer works properly; when I submit, my form data gets emailed to myself and I get redirected to my success page.
However, when I deploy this to Firebase, it seems the nodemailer part doesn't work. The page loads in with my static files, but when I try to submit anything via the form, the page simply refreshes (like when you have a submit button with bare html), instead of redirecting me to my success page and emailing data.
Is there something I need to change in my code to make it work with firebase?
Edit - no logs:
Google requires a paid account in order to make use of "Outbound Networking". The Free Tier does not allow you to make outbound calls. This would include sending mail to a remote mail server (like sending an email to a Yahoo, Gmail, or Outlook account).
See their pricing page for more info.
Look for "Outbound Networking".
If you'd like to leverage Gmail's API, you should still be able to use nodemailer with firebase functions and achieve what you're looking for and remain on the Free Tier. A fully working example is already available in the firebase-samples repository! I would like to highlight what the linked tutorial mentions, which is that Gmail does have an email sending quota that you should be aware of.
I tried to figure out problem in your code but didn't found any, I also have functionality to send email with verification code for authenticate/verify email id. For that I create one gmail id and gave that id/password for sending mail. Mails are sent from that gmail id with node.js when ever user register is email id we send email with veirfication code. My code is as under:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const express = require('express');
var bodyParser = require('body-parser');
var users = require('./users/route');
const app = express();
const nodemailer = require('nodemailer');
// Configure the email transport using the default SMTP transport and a GMail account.
// For other types of transports such as Sendgrid see https://nodemailer.com/transports/
// TODO: Configure the `gmail.email` and `gmail.password` Google Cloud environment variables.
const gmailEmail = 'myapp#gmail.com';
const gmailPassword = 'password';
const gcm = require('node-gcm');
const mailTransport = nodemailer.createTransport(
`smtps://${gmailEmail}:${gmailPassword}#smtp.gmail.com`);
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({extended: false}))
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://myapp.firebaseio.com"
});
//admin.initializeApp(functions.config().firebase);
// Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// when decoded successfully, the ID Token content will be added as `req.user`.
const authenticate = (req, res, next) => {
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer')) {
res.status(403).send('Unauthorized');
return;
}
const idToken = req.headers.authorization.split('Bearer ')[1];
admin.auth().verifyIdToken(idToken).then(decodedIdToken => {
req.user = decodedIdToken;
next();
}).catch(error => {
res.status(403).send('Unauthorized');
});
};
app.get("/token", (req, res) => {
res.status(200).send(admin.auth().applicationDefault());
admin.auth().createCustomToken(req.query.uid)
.then(function (customToken) {
res.status(200).send(customToken);
})
.catch(function (error) {
console.log("Error creating custom token:", error);
});
});
// GET /api/verifyEmail?code="1234"
app.get('/verifyEmail', (req, res) => {
// Get the one-time code from the query parameter.
var verificationCode = req.query.code;
var displayName = req.query.displayName;
var email = req.query.email; //If GET request
const mailOptions = {
from: `Linkuni <noreply#firebase.com>`,
to: email
};
// The user subscribed to the newsletter.
mailOptions.subject = `Welcome to Linkuni`;
mailOptions.text = `Hi ${displayName || ''}\n\n Welcome to Linkuni. We hope you will enjoy our service. Please enter this code:${verificationCode} into the app.\n\nThank you,\nLinkuni Team`;
return mailTransport.sendMail(mailOptions).then(() => {
console.log('Verification email sent to:', email);
});
});
Hope this helps!! :)
I'm using the official Dropbox API (V2) on my Nodejs app.
It sounds like a dumb question but I really can't find out how to get the given access token from the callback url. Actually, it is supposed to be in the hash (#) part of the url (according to their documentation and javascript client-side exemple), which is not visible by the server side...
I can't find any exemple for authentication from a nodejs app, using only the basic api.
Here is my authentication code:
My express app:
//Entry point, DC is a DropboxConnector object
app.get('/connect/Dropbox', function(req, res) {
console.log('/connect/Dropbox called');
res.redirect(DC.getConnexionURL());
});
// Callback from the authentication
app.get('/authDropbox', function(req, res) {
console.log("/authDropbox called");
console.log(url.format(req.protocol + '://' + req.get('host') + req.originalUrl));
// The above log is: 'http://localhost:8080/authDropbox'
// Here is the problem, the access token is unreachable by express
DC.getToken(req.query.code, res);
connectorList.push(DC);
});
DropboxConnector.js, my dropbox api wrapper:
var REDIRECT_URI = 'http://localhost:8080/authDropbox';
//The authentication url given by the dropbox api
getConnexionURL() {
dbx = new Dropbox({ clientId: CLIENT_ID});
var authUrl = dbx.getAuthenticationUrl(REDIRECT_URI);
console.log("AuthURL: " + authUrl);
return authUrl;
}
// #param code is supposed to be the access token...
getToken(code, res) {
if (!!code) {
dbx = new Dropbox({ accessToken: code });
console.log("Authenticated!");
res.redirect(CALLBACK_URL);
} else {
console.log("No code here");
}
}
Thanks for help !
That's correct, the contents of the fragment a.k.a. hash are not visible to the server, only the client (browser). The OAuth 2 "token" flow sends the access token on the fragment, and is mainly meant for client-side apps, e.g., JavaScript in the browser. The OAuth 2 "code" flow instead sends an authorization code as a URL parameter, for server-side apps.
If you're interested, you can find more information on the two different flows in the Dropbox /oauth2/authorize documentation.
The Dropbox API v2 JavaScript SDK unfortunately currently only supports the "token" flow, but we're tracking this as a feature request for support for the "code" flow.
If you do not want to call HTTP directly, you can use my tiny dropbox-v2-api wrapper package:
const dropboxV2Api = require(dropbox-v2-api');
const dropbox = dropboxV2Api.authenticate({
client_id: 'APP_KEY',
client_secret: 'APP_SECRET',
redirect_uri: 'REDIRECT_URI'
});
//generate and visit authorization sevice
const authUrl = dropbox.generateAuthUrl();
//after redirection, you should receive code
dropbox.getToken(code, (err, response) => {
//you are authorized now!
});
Full example (see here):
const dropboxV2Api = require(dropbox-v2-api');
const Hapi = require('hapi');
const fs = require('fs');
const path = require('path');
const Opn = require('opn');
const credentials = JSON.parse(fs.readFileSync(path.join(__dirname, 'credentials.json')));
//set auth credentials
const dropbox = dropboxV2Api.authenticate({
client_id: credentials.APP_KEY,
client_secret: credentials.APP_SECRET,
redirect_uri: 'http://localhost:5000/oauth'
});
//prepare server & oauth2 response callback
const server = new Hapi.Server();
server.connection({ port: 5000 });
server.route({
method: 'GET',
path: '/oauth',
handler: function (request, reply) {
var params = request.query;
dropbox.getToken(params.code, function(err, response){
console.log('user\'s access_token: ',response.access_token);
//call api
dropbox({
resource: 'users/get_current_account'
}, function(err, response){
reply({response: response});
});
});
}
});
server.start(function(){
//open authorization url
Opn(dropbox.generateAuthUrl());
});