I can't find a way to get the token and store it in my Firestore database with the matching uid.
The authorization code is included in the callback url, and I don't see any reason why the 'passport' wouldn't handle it well.
Nonetheless, I manage to get the code by sending a POST request to HubSpot's API, but not via the passport handler.
This is the error I'm getting when testing the code locally:
InternalOAuthError: Failed to obtain access token
at OAuth2Strategy._createOAuthError (/Users/loicgottwalles/Documents/HubSpot_App/node_modules/passport-oauth2/lib/strategy.js:459:17)
at /Users/loicgottwalles/Documents/HubSpot_App/node_modules/passport-oauth2/lib/strategy.js:181:30
at /Users/loicgottwalles/Documents/HubSpot_App/node_modules/oauth/lib/oauth2.js:196:18
at passBackControl (/Users/loicgottwalles/Documents/HubSpot_App/node_modules/oauth/lib/oauth2.js:132:9)
at IncomingMessage.<anonymous> (/Users/loicgottwalles/Documents/HubSpot_App/node_modules/oauth/lib/oauth2.js:157:7)
at IncomingMessage.emit (node:events:530:35)
at endReadableNT (node:internal/streams/readable:1696:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
This is my whole Node.js code:
// Require necessary modules
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const HubSpotStrategy = require('passport-hubspot').Strategy;
const admin = require('firebase-admin');
const crypto = require('crypto');
const axios = require('axios');
const serviceAccount = require('./serviceAccountKey.json');
// Initialize Firebase Admin SDK
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
// Get a reference to the Firestore database
const db = admin.firestore();
// Create an instance of the express application
const app = express();
// Generate a random secret key for session encryption
const secretKey = generateRandomString(32);
// Configure express-session middleware with the random secret key
app.use(session({
secret: secretKey,
resave: false,
saveUninitialized: false
}));
// Configure Passport with the HubSpot strategy
passport.use(new HubSpotStrategy({
clientID: 'xxxxxxxxxx',
clientSecret: 'xxxxxxxxxxx',
callbackURL: 'http://localhost:3000/profile'
},
function(accessToken, refreshToken, profile, done) {
// Pass the access token to the next middleware
return done(null, { id: profile.id, accessToken: accessToken });
}
));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Route to initiate the OAuth2 authentication flow with HubSpot
app.get('/auth/hubspot', (req, res) => {
// Generate a random state value and store it in the session
const state = generateRandomString(32);
req.session.oauthState = state; // Ensure oauthState is set correctly
// Log the oauthState to verify it's set correctly
console.log('OAuth State:', state);
// Capture the Firebase UID from the query parameters
const uid = req.query.uid || null;
// Log the UID for debugging purposes
console.log('Firebase UID:', uid);
// Build the auth URL with state parameter and exclude the Firebase UID
const authUrl =
'https://app.hubspot.com/oauth/authorize' +
`?client_id=${encodeURIComponent('73dc7143-ea7b-47d2-bd34-d343af4fe3f1')}` +
`&redirect_uri=${encodeURIComponent('http://localhost:3000/profile')}` +
'&scope=crm.objects.contacts.read%20crm.objects.contacts.write' +
`&state=${encodeURIComponent(state)}`;
// Redirect the user to the auth URL
console.log('Redirecting user to HubSpot authentication...');
return res.redirect(authUrl);
});
// Callback route to handle the OAuth2 callback from HubSpot
app.get('/profile',
passport.authenticate('hubspot', { failureRedirect: '/homePage' }),
async function(req, res) {
// Check if the state parameter matches the one stored in the session
if (req.query.state !== req.session.oauthState) {
// If not, return a 403 Forbidden response
console.error('State parameter mismatch');
return res.sendStatus(403);
}
try {
// Store the access token in Firestore for the corresponding user ID
await storeAccessToken(req.user.id, req.user.accessToken);
// Successful authentication, set authorized flag in session and redirect to profile page
req.session.authorized = true;
console.log('User authentication successful. Redirecting to profile page...');
res.redirect('/profile');
} catch (error) {
console.error('Error storing access token:', error);
res.status(500).send('Internal Server Error');
}
});
// Proxy route to forward requests to HubSpot's APIs
app.get('/hubspot-proxy', async (req, res) => {
try {
console.log('Making proxy request to HubSpot API...');
const response = await axios.get(req.query.url, { headers: { Authorization: `Bearer ${req.query.accessToken}` } });
res.send(response.data);
} catch (error) {
console.error('Error making proxy request:', error);
res.status(error.response.status).send(error.response.statusText);
}
});
// Start the server and listen on a port
const port = process.env.PORT || 3000; // Use the provided port or default to 3000
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
// Function to store access token in Firestore with additional logging
async function storeAccessToken(uid, accessToken) {
try {
// Log the user ID and access token before storing
console.log('Storing access token for user ID:', uid);
console.log('Access token:', accessToken);
// Store the access token in Firestore for the corresponding user ID
await db.collection('users').doc(uid).set({
accessToken: accessToken
}, { merge: true });
console.log('Access token stored successfully');
} catch (error) {
console.error('Error storing access token:', error);
throw error;
}
}
// Function to generate a random string of specified length
function generateRandomString(length) {
return crypto.randomBytes(Math.ceil(length / 2))
.toString('hex') // Convert to hexadecimal format
.slice(0, length); // Trim to desired length
}
Thank you for your help in advance! Loïc
I tried handling the code with and without the proxy handler. I manage to get the token by sending a POST request to HubSpot's API without any issues, which makes it even more confusing.