Skip to content

Commit b2476be

Browse files
Merge pull request #201 from intuit/release-4.2.2
Fix critical OAuth bugs and improve error handling
2 parents 065ca2b + 18b4d7f commit b2476be

File tree

4 files changed

+261
-81
lines changed

4 files changed

+261
-81
lines changed

sample/app.js

Lines changed: 125 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,28 @@ app.get('/', function (req, res) {
5151
* Get the AuthorizeUri
5252
*/
5353
app.get('/authUri', urlencodedParser, function (req, res) {
54+
// Trim all input values to prevent whitespace issues
55+
const clientId = (req.query.json.clientId || '').trim();
56+
const clientSecret = (req.query.json.clientSecret || '').trim();
57+
const environment = (req.query.json.environment || '').trim();
58+
const redirectUri = (req.query.json.redirectUri || '').trim();
59+
60+
// Validate inputs
61+
if (!clientId || !clientSecret || !environment || !redirectUri) {
62+
return res.status(400).send('Missing required parameters');
63+
}
64+
65+
console.log('\n=== Creating OAuth Client ===');
66+
console.log('Client ID:', clientId.substring(0, 10) + '...');
67+
console.log('Environment:', environment);
68+
console.log('Redirect URI:', redirectUri);
69+
console.log('=============================\n');
70+
5471
oauthClient = new OAuthClient({
55-
clientId: req.query.json.clientId,
56-
clientSecret: req.query.json.clientSecret,
57-
environment: req.query.json.environment,
58-
redirectUri: req.query.json.redirectUri,
72+
clientId,
73+
clientSecret,
74+
environment,
75+
redirectUri,
5976
logging: true, //NOTE: a "logs" folder will be created/used in the current working directory, this will have oAuthClient-log.log
6077
});
6178

@@ -70,13 +87,34 @@ app.get('/authUri', urlencodedParser, function (req, res) {
7087
* Handle the callback to extract the `Auth Code` and exchange them for `Bearer-Tokens`
7188
*/
7289
app.get('/callback', function (req, res) {
90+
console.log('\n=== OAuth Callback Received ===');
91+
console.log('Full callback URL:', req.url);
92+
console.log('Query params:', req.query);
93+
console.log('===============================\n');
94+
7395
oauthClient
7496
.createToken(req.url)
7597
.then(function (authResponse) {
7698
oauth2_token_json = JSON.stringify(authResponse.json, null, 2);
99+
console.log('✅ Token creation successful!');
100+
console.log('Token details:', {
101+
has_access_token: !!authResponse.json.access_token,
102+
has_refresh_token: !!authResponse.json.refresh_token,
103+
realmId: authResponse.json.realmId,
104+
});
77105
})
78106
.catch(function (e) {
79-
console.error(e);
107+
console.error('\n❌ Token creation failed!');
108+
console.error('Error:', e.error || e.message);
109+
console.error('Error description:', e.error_description);
110+
console.error('Intuit TID:', e.intuit_tid);
111+
console.error('Full error:', e);
112+
console.error('\nPossible causes:');
113+
console.error('1. Authorization code already used (codes are single-use)');
114+
console.error('2. Redirect URI mismatch');
115+
console.error('3. Invalid client credentials');
116+
console.error('4. Authorization code expired (10 minute limit)');
117+
console.error('\nSolution: Try authorizing again with "Connect to QuickBooks"\n');
80118
});
81119

82120
res.send('');
@@ -109,13 +147,55 @@ app.get('/refreshAccessToken', function (req, res) {
109147
* getCompanyInfo ()
110148
*/
111149
app.get('/getCompanyInfo', function (req, res) {
112-
const companyID = oauthClient.getToken().realmId;
150+
// Validate that we have a valid oauth client and tokens
151+
if (!oauthClient) {
152+
return res.status(400).json({
153+
error: true,
154+
message: 'OAuth client not initialized. Please connect to QuickBooks first.',
155+
});
156+
}
157+
158+
const token = oauthClient.getToken();
159+
160+
// Check if we have a valid access token
161+
if (!token.access_token) {
162+
return res.status(401).json({
163+
error: true,
164+
message: 'No access token available. Please connect to QuickBooks first.',
165+
hint: 'Click "Connect to QuickBooks" button and complete authorization.',
166+
});
167+
}
168+
169+
// Check if access token is still valid
170+
if (!oauthClient.isAccessTokenValid()) {
171+
return res.status(401).json({
172+
error: true,
173+
message: 'Access token has expired. Please refresh the token or reconnect.',
174+
hint: 'Click "Refresh Token" button to get a new access token.',
175+
});
176+
}
177+
178+
const companyID = token.realmId;
179+
180+
if (!companyID) {
181+
return res.status(400).json({
182+
error: true,
183+
message: 'No company ID (realmId) available.',
184+
});
185+
}
113186

114187
const url =
115188
oauthClient.environment == 'sandbox'
116189
? OAuthClient.environment.sandbox
117190
: OAuthClient.environment.production;
118191

192+
console.log(`\n=== Making API Call ===`);
193+
console.log(`Company ID: ${companyID}`);
194+
console.log(`Environment: ${oauthClient.environment}`);
195+
console.log(`URL: ${url}v3/company/${companyID}/companyinfo/${companyID}`);
196+
console.log(`Access Token Length: ${token.access_token.length}`);
197+
console.log('======================\n');
198+
119199
oauthClient
120200
.makeApiCall({ url: `${url}v3/company/${companyID}/companyinfo/${companyID}` })
121201
.then(function (authResponse) {
@@ -124,14 +204,31 @@ app.get('/getCompanyInfo', function (req, res) {
124204
res.send(resp);
125205
})
126206
.catch(function (e) {
207+
// Check if it's an OAuthError with detailed information
208+
console.error('\n=== API Call Error ===');
209+
console.error('Error Name:', e.name);
210+
console.error('Error Message:', e.message);
211+
212+
if (e.code) {
213+
console.error('Error Code:', e.code);
214+
}
215+
216+
if (e.description) {
217+
console.error('Error Description:', e.description);
218+
}
219+
220+
if (e.intuitTid) {
221+
console.error('Intuit Transaction ID:', e.intuitTid);
222+
}
223+
127224
// Detailed error analysis
128225
const errorAnalysis = {
129226
// Basic error properties
130227
basic: {
131228
name: e.name,
132229
message: e.message,
133230
stack: e.stack,
134-
code: e.code
231+
code: e.code,
135232
},
136233
// Response analysis
137234
response: e.response ? {
@@ -148,41 +245,49 @@ app.get('/getCompanyInfo', function (req, res) {
148245
detail: err.Detail,
149246
code: err.code,
150247
element: err.element,
151-
additionalInfo: err.additionalInfo
248+
additionalInfo: err.additionalInfo,
152249
})) : null,
153-
timestamp: e.response.data.time
250+
timestamp: e.response.data.time,
154251
} : null),
155252
// OAuth error fields
156253
oauth: {
157-
error:e.response.data && e.response.data.error,
158-
error_description: e.response.data && e.response.data.error_description
159-
}
254+
error: e.response.data && e.response.data.error,
255+
error_description: e.response.data && e.response.data.error_description,
256+
},
160257
} : null,
161258
// Request analysis
162259
request: e.request ? {
163260
method: e.request.method,
164261
path: e.request.path,
165-
headers: e.request.headers
166-
} : null
262+
headers: e.request.headers,
263+
} : null,
167264
};
168265

169266
// Log the detailed error analysis
170267
console.error('Exception Analysis:', {
171268
hasFaultObject: !!(e.response && e.response.data && e.response.data.Fault),
172269
faultType: e.response && e.response.data && e.response.data.Fault && e.response.data.Fault.type,
173270
faultErrors: e.response && e.response.data && e.response.data.Fault && e.response.data.Fault.Error,
174-
fullAnalysis: errorAnalysis
271+
fullAnalysis: errorAnalysis,
175272
});
273+
274+
console.error('======================\n');
176275

177-
// Send error response to client
178-
res.status(e.response ? e.response.status : 500).json({
276+
// Send error response to client with more detail
277+
const status = e.response ? e.response.status : 500;
278+
const errorResponse = {
179279
error: true,
180280
message: e.message,
281+
code: e.code,
282+
description: e.description,
283+
intuitTid: e.intuitTid,
181284
fault: e.response && e.response.data && e.response.data.Fault ? {
182285
type: e.response.data.Fault.type,
183-
errors: e.response.data.Fault.Error
184-
} : null
185-
});
286+
errors: e.response.data.Fault.Error,
287+
} : null,
288+
};
289+
290+
res.status(status).json(errorResponse);
186291
});
187292
});
188293

sample/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"ejs": "^3.1.9",
1818
"express": "^4.14.0",
1919
"express-session": "^1.14.2",
20-
"intuit-oauth": "^4.1.0",
20+
"intuit-oauth": "4.2.2",
2121
"ngrok": "^5.0.0-beta.2",
2222
"path": "^0.12.7"
2323
},

src/OAuthClient.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,16 +202,31 @@ function safeStringify(obj) {
202202
OAuthClient.prototype.createToken = function createToken(uri) {
203203
return new Promise((resolve) => {
204204
if (!uri) throw new Error('Provide the Uri');
205-
const params = queryString.parse(uri.split('?').reverse()[0]);
206-
this.getToken().realmId = params.realmId ? params.realmId : '';
207-
if ('state' in params) this.getToken().state = params.state;
205+
// Safely parse query string from URI
206+
const queryIndex = uri.indexOf('?');
207+
const queryPart = queryIndex !== -1 ? uri.substring(queryIndex + 1) : uri;
208+
const params = queryString.parse(queryPart);
209+
210+
// Fix: Directly access token object instead of getToken() which returns a copy
211+
this.token.realmId = params.realmId || '';
212+
if ('state' in params) this.token.state = params.state;
208213

209214
const body = {};
210215
if (params.code) {
211216
body.grant_type = 'authorization_code';
212217
body.code = params.code;
213218
body.redirect_uri = params.redirectUri || this.redirectUri;
214219
}
220+
221+
// Log request details for debugging (without sensitive data)
222+
this.log('info', 'Token exchange request:', {
223+
grant_type: body.grant_type,
224+
code_length: body.code ? body.code.length : 0,
225+
redirect_uri: body.redirect_uri,
226+
has_code: !!body.code,
227+
realmId: this.token.realmId,
228+
state: this.token.state,
229+
});
215230

216231
const request = {
217232
url: OAuthClient.tokenEndpoint,
@@ -680,13 +695,19 @@ OAuthClient.prototype.validateIdToken = function validateIdToken(params = {}) {
680695
const id_token_payload = JSON.parse(atob(token_parts[1]));
681696

682697
// Step 1 : First check if the issuer is as mentioned in "issuer"
683-
if (id_token_payload.iss !== 'https://oauth.platform.intuit.com/op/v1') return false;
698+
if (id_token_payload.iss !== 'https://oauth.platform.intuit.com/op/v1') {
699+
throw new Error('Invalid issuer in ID token');
700+
}
684701

685702
// Step 2 : check if the aud field in idToken contains application's clientId
686-
if (!id_token_payload.aud.find((audience) => audience === this.clientId)) return false;
703+
if (!id_token_payload.aud.find((audience) => audience === this.clientId)) {
704+
throw new Error('Invalid audience in ID token');
705+
}
687706

688707
// Step 3 : ensure the timestamp has not elapsed
689-
if (id_token_payload.exp < Date.now() / 1000) return false;
708+
if (id_token_payload.exp < Date.now() / 1000) {
709+
throw new Error('ID token has expired');
710+
}
690711

691712
const request = {
692713
url: OAuthClient.jwks_uri,
@@ -726,6 +747,9 @@ OAuthClient.prototype.getKeyFromJWKsURI = function getKeyFromJWKsURI(id_token, k
726747
if (Number(response.status) !== 200) throw new Error('Could not reach JWK endpoint');
727748
// Find the key by KID
728749
const key = response.data.keys.find((el) => el.kid === kid);
750+
if (!key) {
751+
throw new Error(`Key with kid "${kid}" not found in JWKS`);
752+
}
729753
const cert = this.getPublicKey(key.n, key.e);
730754

731755
return jwt.verify(id_token, cert);

0 commit comments

Comments
 (0)