Dropbox api V2, get access token in query param instead of url hash (#) (Nodejs) - node.js

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());
});

Related

Authentication using Node.js OauthClient "auth-code" flow

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`)
});

Oauth 2.0 redirect to app after auth on server

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.

Spotify API Authorization redirects too many times

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.

Chaining a Get request from a Post request then sending to client, using Express and Google oauth

I am servicing a Post request, where I retrieve user token id in this request, I further verify the token using OAuth2Client library. When verified, I want to issue a Get request to get user info. I am not sure of the way I am chaining requests.
Code is like:
var userid;
app.post( '/oauth/google/redirect', (req, res) => {
const token = req.body.idtoken;
verify().catch(console.error);
req.get(`https://oauth2.googleapis.com/tokeninfo?id_token=${token}`, (req, res) => {
var body = "";
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
res.send(body)
});
})
});
const {OAuth2Client} = require('google-auth-library');
const client = new OAuth2Client(google.clientID);
async function verify() {
const ticket = await client.verifyIdToken({
idToken: token,
audience: google.clientID, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
const payload = ticket.getPayload();
userid = payload['sub'];
// If request specified a G Suite domain:
//const domain = payload['hd'];
}
On client side, browser says it failed on post request as it took too much time returning:
POST http://mywebapp.com/oauth/google/redirect 504 (Gateway Time-out)
Signed in as: <html><body><h1>504 Gateway Time-out</h1>
The server didn't respond in time.
</body></html>
For now, I used request as a library, something like:
app.post( '/oauth/google/redirect', (req, res) => {
const token = req.body.idtoken;
verify().catch(console.error);
request(`https://oauth2.googleapis.com/tokeninfo?id_token=${token}`, function (error, response, body) {
var data={
body:body
};
res.send(data);
});
});
Still accepting a native Express solution as a better answer.

Coinbase Connect (OAuth2) "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."

Dear stackoverflowser,
I'm stying to build an Electron app with Coinbase intergration.
First I'm going to make the server (NodeJS) work with OAuth2.
Every thing works great, but when I want to change the code into an access token with the instructed post request it gives me the following error:
{
error: "invalid_request",
error_description: "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."
}
I already added https://localhost:3000/auth/coinbase/callback and https://localhost:3000/profile to the valid API URI's.
I wasn't successful in figuring it out after several hours.
My server is this:
var express = require('express');
var app = express();
var fs = require('fs')
var https = require('https');
var coinbase = require('coinbase')
var request = require('request');
var options = {
key: fs.readFileSync('./ssl/coinbase.dev.key'),
cert: fs.readFileSync('./ssl/coinbase.dev.crt'),
};
var client_id = 'gues it'
var client_secret = 'gues it'
app.use(express.static('static'));
app.get('/login/coinbase', function(req, res) {
res.redirect('https://www.coinbase.com/oauth/authorize?response_type=code&redirect_uri=https://localhost:3000/auth/coinbase/callback&client_id=' + client_id + '&scope=wallet:user:read,wallet:accounts:read')
})
app.get('/auth/coinbase/callback', function(req, res) {
var data = {
client_id: client_id,
client_secret: client_secret,
grant_type: 'authorization_code',
code: req.query.code,
redirect_uri: 'https://localhost:3000/profile'
}
request.post(
'https://api.coinbase.com/oauth/token', data, function (error, response, body) {
console.log(body)
res.send(body)
}
);
})
app.get('/', function(req, res) {
res.send('home')
})
app.get('/profile', function(req, res) {
res.send('profile')
})
var server = https.createServer(options, app);
server.listen(3000)
Thanks in advance,
Theo
[EDIT]
I contacted the Coinbase developers and they were surprised that there was no NodeJS example on OAuth with Coinbase, so they added it to their roadmap.
This is most likely caused by one of the following:
You do not have 'http://127.0.0.1:3000/profile' listed as a valid redirect API in your application settings.
You are reusing an authorization code that has already been exchanged for a token.
This section of this page:
OAuth2 Redirect URI
For added security, all redirect_uris must use SSL (i.e. begin with
https://). URIs without SSL can only be used for development and
testing and will not be supported in production.
Contact api#coinbase.com to get that sorted out.

Resources