├── .gitignore ├── LICENSE.txt ├── README.md ├── app.js ├── config.json ├── package.json ├── public ├── C2QB_white_btn_lg_default.png ├── C2QB_white_btn_lg_hover.png ├── IntuitSignIn-lg-white@2x.jpg └── style.css ├── routes ├── api_call.js ├── callback.js ├── connect_handler.js ├── connect_to_quickbooks.js ├── connected.js └── sign_in_with_intuit.js ├── tools ├── jwt.js ├── openid_configuration.json └── tools.js └── views ├── Callout.png ├── Sample.png ├── connected.ejs └── home.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Intuit, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Sample Banner](views/Sample.png)][ss1] 2 | 3 | ## OAuth 2.0 - Node.js Sample App 4 | 5 | The [Intuit Developer team](https://developer.intuit.com) has written this OAuth 2.0 Sample App in Node.js to provide working examples of OAuth 2.0 concepts, and how to integrate with Intuit endpoints. 6 | 7 | 8 | ### Getting Started 9 | 10 | Before beginning, it may be helpful to have a basic understanding of OAuth 2.0 concepts. There are plenty of tutorials and guides to get started with OAuth 2.0. 11 | 12 | It is also expected that your development environment is properly set up for Node.js and NPM. 13 | 14 | Note: this app was tested with Node.js versions v6.0.0, v7.0.0, and v8.0.0. 15 | 16 | #### Setup 17 | 18 | Clone the repository: 19 | ``` 20 | git clone https://github.com/IntuitDeveloper/oauth2-nodejs.git 21 | ``` 22 | 23 | Install NPM dependencies: 24 | ``` 25 | cd oauth2-nodejs 26 | npm install 27 | ``` 28 | 29 | Launch your app: 30 | ``` 31 | node app.js 32 | ``` 33 | 34 | Your app should be running! If you direct your browser to `https://localhost:3000`, you should see the welcome screen. Please note - the app will not be fully functional until we finish configuring it. 35 | 36 | ### Configuring your app 37 | 38 | All configuration for this app is located in `config.json`. Locate and open this file. 39 | 40 | We will need to update 3 items: 41 | 42 | - `clientId` 43 | - `clientSecret` 44 | - `redirectUri` 45 | 46 | All of these values must match **exactly** with what is listed in your app settings on [developer.intuit.com](https://developer.intuit.com). If you haven't already created an app, you may do so there. Please read on for important notes about client credentials, scopes, and redirect urls. 47 | 48 | #### Client Credentials 49 | 50 | Once you have created an app on Intuit's Developer Portal, you can find your credentials (Client ID and Client Secret) under the "Keys" section. These are the values you'll have to copy into `config.json`. 51 | 52 | #### Redirect URI 53 | 54 | You'll have to set a Redirect URI in both `config.json` *and* the Developer Portal ("Keys" section). With this app, the typical value would be `http://localhost:3000/callback`, unless you host this sample app in a different way (if you were testing HTTPS, for example). 55 | 56 | **Note:** Using `localhost` and `http` will only work when developing, using the sandbox credentials. Once you use production credentials, you'll need to host your app over `https`. 57 | 58 | #### Scopes 59 | 60 | While you are in `config.json`, you'll notice the scope sections. 61 | 62 | ``` 63 | "scopes": { 64 | "sign_in_with_intuit": [ 65 | "openid", 66 | ... 67 | ], 68 | "connect_to_quickbooks": [ 69 | "com.intuit.quickbooks.accounting", 70 | "com.intuit.quickbooks.payment" 71 | ], 72 | "connect_handler": [ 73 | "com.intuit.quickbooks.accounting", 74 | "com.intuit.quickbooks.payment", 75 | "openid", 76 | ... 77 | ] 78 | }, 79 | ``` 80 | It is important to ensure that the scopes you are requesting match the scopes allowed on the Developer Portal. For this sample app to work by default, your app on Developer Portal must support both Accounting and Payment scopes. If you'd like to support Accounting only, simply remove the`com.intuit.quickbooks.payment` scope from `config.json`. 81 | 82 | ---------- 83 | 84 | ### Run your app! 85 | 86 | After setting up both Developer Portal and your `config.json`, try launching your app again! 87 | ``` 88 | node app.js 89 | ``` 90 | All flows should work. The sample app supports the following flows: 91 | 92 | **Sign In With Intuit** - this flow requests OpenID only scopes. Feel free to change the scopes being requested in `config.json`. After authorizing (or if the account you are using has already been authorized for this app), the redirect URL (`/callback`) will parse the JWT ID token, and make an API call to the user information endpoint. 93 | 94 | **Connect To QuickBooks** - this flow requests non-OpenID scopes. You will be able to make a QuickBooks API sample call (using the OAuth2 token) on the `/connected` landing page. 95 | 96 | **Get App Now (Connect Handler)** - this flow requests both OpenID and non-OpenID scopes. It simulates the request that would come once a user clicks "Get App Now" on the [apps.com](https://apps.com) website, after you publish your app. 97 | 98 | ---------- 99 | 100 | ### Project Structure 101 | 102 | In order to find the code snippets you are interested in, here is how the code is organized. 103 | 104 | #### Launching the OAuth2 flow 105 | 106 | Examples of launching the OAuth2 flow, including passing the right parameters and generating CSRF ant-forgery tokens, can be found in: 107 | 108 | ``` 109 | /routes/sign_in_with_intuit.js 110 | /routes/connect_to_quickbooks.js 111 | /routes/connect_handler.js 112 | ``` 113 | 114 | #### Callback URL 115 | 116 | `/routes/callback.js` contains code snippets that receive the authorization code, make the bearer token exchange, and validate the JWT ID token (if applicable). It then redirects to the post-connection landing page, `/routes/connected.js`. 117 | 118 | #### Connected 119 | `/routes/connected.js` will make an example OpenID user information call over OAuth2 (assuming the openid scopes were requested). Once loaded, the page allows you to make AJAX API calls over OAuth2. 120 | 121 | #### API Calls 122 | 123 | `/routes/api_call.js` allows three different API calls to be made over OAuth2: 124 | 125 | - **QBO Call** - make an example accounting API call (note: this endpoint comes from `config.json`. The endpoint is different for sandbox versus non-sandbox. Make sure your `config.json` contains the correct endpoint!) 126 | - **Refresh Call** - use the refresh token to get a new access token. 127 | - **Revoke Call** - revoke the access token, so it no longer can access APIs. 128 | 129 | View these code snippets to see how to correctly pass the access token or client credentials (depending on the API call). 130 | 131 | #### JWT (ID Token) 132 | 133 | `/tools/jwt.js` - For OpenID scopes, after exchanging the authorization code, you will receive a JWT (JSON Web Token) ID Token. View this code snippet for an example of how to decode, and validate that the ID Token is secure. 134 | 135 | [ss1]: https://help.developer.intuit.com/s/samplefeedback?cid=9010&repoName=oauth2-nodejs 136 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('./config.json') 3 | var express = require('express') 4 | var session = require('express-session') 5 | var app = express() 6 | 7 | app.set('views', path.join(__dirname, 'views')) 8 | app.set('view engine', 'ejs') 9 | app.use(express.static(path.join(__dirname, 'public'))) 10 | app.use(session({secret: 'secret', resave: 'false', saveUninitialized: 'false'})) 11 | 12 | // Initial view - loads Connect To QuickBooks Button 13 | app.get('/', function (req, res) { 14 | res.render('home', config) 15 | }) 16 | 17 | // Sign In With Intuit, Connect To QuickBooks, or Get App Now 18 | // These calls will redirect to Intuit's authorization flow 19 | app.use('/sign_in_with_intuit', require('./routes/sign_in_with_intuit.js')) 20 | app.use('/connect_to_quickbooks', require('./routes/connect_to_quickbooks.js')) 21 | app.use('/connect_handler', require('./routes/connect_handler.js')) 22 | 23 | // Callback - called via redirect_uri after authorization 24 | app.use('/callback', require('./routes/callback.js')) 25 | 26 | // Connected - call OpenID and render connected view 27 | app.use('/connected', require('./routes/connected.js')) 28 | 29 | // Call an example API over OAuth2 30 | app.use('/api_call', require('./routes/api_call.js')) 31 | 32 | 33 | // Start server on HTTP (will use ngrok for HTTPS forwarding) 34 | app.listen(3000, function () { 35 | console.log('Example app listening on port 3000!') 36 | }) 37 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientId": "", 3 | "clientSecret": "", 4 | "redirectUri": "https:///callback", 5 | "configurationEndpoint": "https://developer.api.intuit.com/.well-known/openid_sandbox_configuration/", 6 | "api_uri": "https://sandbox-quickbooks.api.intuit.com/v3/company/", 7 | "scopes": { 8 | "sign_in_with_intuit": [ 9 | "openid", 10 | "profile", 11 | "email", 12 | "phone", 13 | "address" 14 | ], 15 | "connect_to_quickbooks": [ 16 | "com.intuit.quickbooks.accounting", 17 | "com.intuit.quickbooks.payment" 18 | ], 19 | "connect_handler": [ 20 | "com.intuit.quickbooks.accounting", 21 | "com.intuit.quickbooks.payment", 22 | "openid", 23 | "profile", 24 | "email", 25 | "phone", 26 | "address" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2-nodejs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "atob": "^2.0.3", 14 | "btoa": "^1.1.2", 15 | "client-oauth2": "^3.2.0", 16 | "csrf": "^3.0.4", 17 | "ejs": "^2.5.2", 18 | "expect": "^1.20.2", 19 | "express": "^4.14.0", 20 | "express-session": "^1.14.2", 21 | "fs": "0.0.1-security", 22 | "https": "^1.0.0", 23 | "jsonwebtoken": "^7.1.9", 24 | "path": "^0.12.7", 25 | "request": "^2.78.0", 26 | "rsa-pem-from-mod-exp": "^0.8.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/C2QB_white_btn_lg_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/oauth2-nodejs/22f1093174db797d01badcfe4cdd42ff3039649f/public/C2QB_white_btn_lg_default.png -------------------------------------------------------------------------------- /public/C2QB_white_btn_lg_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/oauth2-nodejs/22f1093174db797d01badcfe4cdd42ff3039649f/public/C2QB_white_btn_lg_hover.png -------------------------------------------------------------------------------- /public/IntuitSignIn-lg-white@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/oauth2-nodejs/22f1093174db797d01badcfe4cdd42ff3039649f/public/IntuitSignIn-lg-white@2x.jpg -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue",HelveticaNeue,Helvetica,Arial,sans-serif; 3 | } 4 | 5 | a:visited { 6 | color: #0000ee; 7 | } 8 | 9 | .imgLink { 10 | text-decoration: none; 11 | display: inline-block; 12 | margin: 10px; 13 | } 14 | 15 | #result { 16 | white-space: pre-wrap; 17 | } 18 | -------------------------------------------------------------------------------- /routes/api_call.js: -------------------------------------------------------------------------------- 1 | var tools = require('../tools/tools.js') 2 | var config = require('../config.json') 3 | var request = require('request') 4 | var express = require('express') 5 | var router = express.Router() 6 | 7 | /** /api_call **/ 8 | router.get('/', function (req, res) { 9 | var token = tools.getToken(req.session) 10 | if(!token) return res.json({error: 'Not authorized'}) 11 | if(!req.session.realmId) return res.json({ 12 | error: 'No realm ID. QBO calls only work if the accounting scope was passed!' 13 | }) 14 | 15 | // Set up API call (with OAuth2 accessToken) 16 | var url = config.api_uri + req.session.realmId + '/companyinfo/' + req.session.realmId 17 | console.log('Making API call to: ' + url) 18 | var requestObj = { 19 | url: url, 20 | headers: { 21 | 'Authorization': 'Bearer ' + token.accessToken, 22 | 'Accept': 'application/json' 23 | } 24 | } 25 | 26 | // Make API call 27 | request(requestObj, function (err, response) { 28 | // Check if 401 response was returned - refresh tokens if so! 29 | tools.checkForUnauthorized(req, requestObj, err, response).then(function ({err, response}) { 30 | if(err || response.statusCode != 200) { 31 | return res.json({error: err, statusCode: response.statusCode}) 32 | } 33 | 34 | // API Call was a success! 35 | res.json(JSON.parse(response.body)) 36 | }, function (err) { 37 | console.log(err) 38 | return res.json(err) 39 | }) 40 | }) 41 | }) 42 | 43 | /** /api_call/revoke **/ 44 | router.get('/revoke', function (req, res) { 45 | var token = tools.getToken(req.session) 46 | if(!token) return res.json({error: 'Not authorized'}) 47 | 48 | var url = tools.revoke_uri 49 | request({ 50 | url: url, 51 | method: 'POST', 52 | headers: { 53 | 'Authorization': 'Basic ' + tools.basicAuth, 54 | 'Accept': 'application/json', 55 | 'Content-Type': 'application/json' 56 | }, 57 | body: JSON.stringify({ 58 | 'token': token.accessToken 59 | }) 60 | }, function (err, response, body) { 61 | if(err || response.statusCode != 200) { 62 | return res.json({error: err, statusCode: response.statusCode}) 63 | } 64 | tools.clearToken(req.session) 65 | res.json({response: "Revoke successful"}) 66 | }) 67 | }) 68 | 69 | /** /api_call/refresh **/ 70 | // Note: typical use case would be to refresh the tokens internally (not an API call) 71 | // We recommend refreshing upon receiving a 401 Unauthorized response from Intuit. 72 | // A working example of this can be seen above: `/api_call` 73 | router.get('/refresh', function (req, res) { 74 | var token = tools.getToken(req.session) 75 | if(!token) return res.json({error: 'Not authorized'}) 76 | 77 | tools.refreshTokens(req.session).then(function(newToken) { 78 | // We have new tokens! 79 | res.json({ 80 | accessToken: newToken.accessToken, 81 | refreshToken: newToken.refreshToken 82 | }) 83 | }, function(err) { 84 | // Did we try to call refresh on an old token? 85 | console.log(err) 86 | res.json(err) 87 | }) 88 | }) 89 | 90 | module.exports = router 91 | -------------------------------------------------------------------------------- /routes/callback.js: -------------------------------------------------------------------------------- 1 | var tools = require('../tools/tools.js') 2 | var jwt = require('../tools/jwt.js') 3 | var express = require('express') 4 | var router = express.Router() 5 | 6 | /** /callback **/ 7 | router.get('/', function (req, res) { 8 | // Verify anti-forgery 9 | if(!tools.verifyAntiForgery(req.session, req.query.state)) { 10 | return res.send('Error - invalid anti-forgery CSRF response!') 11 | } 12 | 13 | 14 | // Exchange auth code for access token 15 | tools.intuitAuth.code.getToken(req.originalUrl).then(function (token) { 16 | // Store token - this would be where tokens would need to be 17 | // persisted (in a SQL DB, for example). 18 | tools.saveToken(req.session, token) 19 | req.session.realmId = req.query.realmId 20 | 21 | var errorFn = function(e) { 22 | console.log('Invalid JWT token!') 23 | console.log(e) 24 | res.redirect('/') 25 | } 26 | 27 | if(token.data.id_token) { 28 | try { 29 | // We should decode and validate the ID token 30 | jwt.validate(token.data.id_token, function() { 31 | // Callback function - redirect to /connected 32 | res.redirect('connected') 33 | }, errorFn) 34 | } catch (e) { 35 | errorFn(e) 36 | } 37 | } else { 38 | // Redirect to /connected 39 | res.redirect('connected') 40 | } 41 | }, function (err) { 42 | console.log(err) 43 | res.send(err) 44 | }) 45 | }) 46 | 47 | module.exports = router 48 | -------------------------------------------------------------------------------- /routes/connect_handler.js: -------------------------------------------------------------------------------- 1 | var tools = require('../tools/tools.js') 2 | var express = require('express') 3 | var router = express.Router() 4 | 5 | /** /connect_handler **/ 6 | // This would be the endpoint that is called when "Get App Now" is clicked 7 | // from apps.com 8 | router.get('/', function (req, res) { 9 | // Set the OpenID + Accounting + Payment scopes 10 | tools.setScopes('connect_handler') 11 | 12 | // Constructs the authorization URI. 13 | var uri = tools.intuitAuth.code.getUri({ 14 | // Add CSRF protection 15 | state: tools.generateAntiForgery(req.session) 16 | }) 17 | 18 | // Redirect 19 | console.log('Redirecting to authorization uri: ' + uri) 20 | res.redirect(uri) 21 | }) 22 | 23 | module.exports = router 24 | -------------------------------------------------------------------------------- /routes/connect_to_quickbooks.js: -------------------------------------------------------------------------------- 1 | var tools = require('../tools/tools.js') 2 | var express = require('express') 3 | var router = express.Router() 4 | 5 | /** /connect_to_quickbooks **/ 6 | router.get('/', function (req, res) { 7 | // Set the Accounting + Payment scopes 8 | tools.setScopes('connect_to_quickbooks') 9 | 10 | // Constructs the authorization URI. 11 | var uri = tools.intuitAuth.code.getUri({ 12 | // Add CSRF protection 13 | state: tools.generateAntiForgery(req.session) 14 | }) 15 | 16 | // Redirect 17 | console.log('Redirecting to authorization uri: ' + uri) 18 | res.redirect(uri) 19 | }) 20 | 21 | module.exports = router 22 | -------------------------------------------------------------------------------- /routes/connected.js: -------------------------------------------------------------------------------- 1 | var tools = require('../tools/tools.js') 2 | var https = require('https') 3 | var url = require('url') 4 | var express = require('express') 5 | var router = express.Router() 6 | 7 | router.get('/', function (req, res) { 8 | var token = tools.getToken(req.session) 9 | if(!token) return res.redirect('/') 10 | 11 | // Don't call OpenID if we didn't request OpenID scopes 12 | if(!tools.containsOpenId()) return res.render('connected') 13 | 14 | // Call OpenID endpoint 15 | // (this example uses the raw `https` npm module) 16 | // (see api_call.js for example using helper `request` npm module) 17 | var options = token.sign(url.parse(tools.openid_uri)) 18 | var request = https.request(options, (response) => { 19 | response.setEncoding('utf8'); 20 | let rawData = ''; 21 | response.on('data', (chunk) => rawData += chunk); 22 | response.on('end', () => { 23 | console.log('OpenID response: ' + rawData) 24 | try { 25 | var parsedData = JSON.parse(rawData) 26 | res.render('connected', parsedData) 27 | } catch (e) { 28 | console.log(e.message) 29 | res.render('connected') 30 | } 31 | }); 32 | }); 33 | request.end(); 34 | 35 | request.on('error', (e) => { 36 | console.error(e) 37 | res.send(e) 38 | }) 39 | }) 40 | 41 | module.exports = router 42 | -------------------------------------------------------------------------------- /routes/sign_in_with_intuit.js: -------------------------------------------------------------------------------- 1 | var tools = require('../tools/tools.js') 2 | var express = require('express') 3 | var router = express.Router() 4 | 5 | /** /sign_in_with_intuit **/ 6 | router.get('/', function (req, res) { 7 | // Set the OpenID scopes 8 | tools.setScopes('sign_in_with_intuit') 9 | 10 | // Constructs the authorization URI. 11 | var uri = tools.intuitAuth.code.getUri({ 12 | // Add CSRF protection 13 | state: tools.generateAntiForgery(req.session) 14 | }) 15 | 16 | // Redirect 17 | console.log('Redirecting to authorization uri: ' + uri) 18 | res.redirect(uri) 19 | }) 20 | 21 | module.exports = router 22 | -------------------------------------------------------------------------------- /tools/jwt.js: -------------------------------------------------------------------------------- 1 | var atob = require('atob') 2 | var expect = require('expect') 3 | var request = require('request') 4 | var tools = require('./tools') 5 | var config = require('../config.json') 6 | 7 | var JWT = function () { 8 | var jwt = this; 9 | 10 | // Performs the correct JWT validation steps 11 | this.validate = function(id_token, callback, errorFn) { 12 | // https://developer.api.intuit.com/.well-known/openid_configuration/ 13 | var openid_configuration = tools.openid_configuration 14 | 15 | // Decode ID Token 16 | var token_parts = id_token.split('.') 17 | var idTokenHeader = JSON.parse(atob(token_parts[0])) 18 | var idTokenPayload = JSON.parse(atob(token_parts[1])) 19 | var idTokenSignature = atob(token_parts[2]) 20 | 21 | // Step 1 : First check if the issuer is as mentioned in "issuer" in the discovery doc 22 | expect(idTokenPayload.iss).toEqual(openid_configuration.issuer) 23 | 24 | // Step 2 : check if the aud field in idToken is same as application's clientId 25 | expect(idTokenPayload.aud).toEqual(config.clientId) 26 | 27 | // Step 3 : ensure the timestamp has not elapsed 28 | expect(idTokenPayload.exp).toBeGreaterThan(Date.now() / 1000) 29 | 30 | // Step 4: Verify that the ID token is properly signed by the issuer 31 | jwt.getKeyFromJWKsURI(idTokenHeader.kid, function(key) { 32 | var cert = jwt.getPublicKey(key.n, key.e) 33 | // Validate the RSA encryption 34 | require("jsonwebtoken").verify(id_token, cert, function(err) { 35 | if(err) errorFn(err) 36 | else callback() 37 | }) 38 | }) 39 | } 40 | 41 | // Loads the correct key from JWKs URI: 42 | // https://oauth.platform.intuit.com/op/v1/jwks 43 | this.getKeyFromJWKsURI = function(kid, callback) { 44 | var openid_configuration = tools.openid_configuration 45 | 46 | request({ 47 | url: openid_configuration.jwks_uri, 48 | json: true 49 | }, function(error, response, body) { 50 | if(error || response.statusCode != 200) { 51 | throw new Error("Could not reach JWK endpoint") 52 | } 53 | // Find the key by KID 54 | var key = body.keys.find(el => (el.kid == kid)) 55 | callback(key) 56 | }) 57 | } 58 | 59 | // Creates a PEM style RSA public key, using the modulus (n) and exponent (e) 60 | this.getPublicKey = function(modulus, exponent) { 61 | var getPem = require('rsa-pem-from-mod-exp') 62 | var pem = getPem(modulus, exponent) 63 | return pem 64 | } 65 | } 66 | 67 | module.exports = new JWT(); 68 | -------------------------------------------------------------------------------- /tools/openid_configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "issuer":"https://oauth.platform.intuit.com/op/v1", 3 | "authorization_endpoint":"https://appcenter.intuit.com/connect/oauth2", 4 | "token_endpoint":"https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer", 5 | "userinfo_endpoint":"https://sandbox-accounts.platform.intuit.com/v1/openid_connect/userinfo", 6 | "revocation_endpoint":"https://developer.api.intuit.com/v2/oauth2/tokens/revoke", 7 | "jwks_uri":"https://oauth.platform.intuit.com/op/v1/jwks", 8 | "response_types_supported":[ 9 | "code" 10 | ], 11 | "subject_types_supported":[ 12 | "public" 13 | ], 14 | "id_token_signing_alg_values_supported":[ 15 | "RS256" 16 | ], 17 | "scopes_supported":[ 18 | "openid", 19 | "email", 20 | "profile", 21 | "address", 22 | "phone" 23 | ], 24 | "token_endpoint_auth_methods_supported":[ 25 | "client_secret_post", 26 | "client_secret_basic" 27 | ], 28 | "claims_supported":[ 29 | "aud", 30 | "exp", 31 | "iat", 32 | "iss", 33 | "realmid", 34 | "sub" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tools/tools.js: -------------------------------------------------------------------------------- 1 | var Tokens = require('csrf') 2 | var csrf = new Tokens() 3 | var ClientOAuth2 = require('client-oauth2') 4 | var request = require('request') 5 | var config = require('../config.json') 6 | 7 | var Tools = function () { 8 | var tools = this; 9 | 10 | var authConfig = { 11 | clientId: config.clientId, 12 | clientSecret: config.clientSecret, 13 | redirectUri: config.redirectUri 14 | } 15 | 16 | this.basicAuth = require('btoa')(authConfig.clientId + ':' + authConfig.clientSecret) 17 | 18 | // Use a local copy for startup. This will be updated in refreshEndpoints() to call: 19 | // https://developer.api.intuit.com/.well-known/openid_configuration/ 20 | this.openid_configuration = require('./openid_configuration.json') 21 | 22 | // Should be called at app start & scheduled to run once a day 23 | // Get the latest OAuth/OpenID endpoints from Intuit 24 | this.refreshEndpoints = function() { 25 | request({ 26 | // Change this to Sandbox or non-sandbox in `config.json` 27 | // Non-sandbox: https://developer.api.intuit.com/.well-known/openid_configuration/ 28 | // Sandbox: https://developer.api.intuit.com/.well-known/openid_sandbox_configuration/ 29 | url: config.configurationEndpoint, 30 | headers: { 31 | 'Accept': 'application/json' 32 | } 33 | 34 | }, function(err, response) { 35 | if(err) { 36 | console.log(err) 37 | return err 38 | } 39 | 40 | // Update endpoints 41 | var json = JSON.parse(response.body) 42 | tools.openid_configuration = json 43 | tools.openid_uri = json.userinfo_endpoint 44 | tools.revoke_uri = json.revocation_endpoint 45 | 46 | // Re-create OAuth2 Client 47 | authConfig.authorizationUri = json.authorization_endpoint 48 | authConfig.accessTokenUri = json.token_endpoint 49 | tools.intuitAuth = new ClientOAuth2(authConfig) 50 | }) 51 | } 52 | 53 | // Should be used to check for 401 response when making an API call. If a 401 54 | // response is received, refresh tokens should be used to get a new access token, 55 | // and the API call should be tried again. 56 | this.checkForUnauthorized = function(req, requestObj, err, response) { 57 | return new Promise(function (resolve, reject) { 58 | if(response.statusCode == 401) { 59 | console.log('Received a 401 response! Trying to refresh tokens.') 60 | 61 | // Refresh the tokens 62 | tools.refreshTokens(req.session).then(function(newToken) { 63 | // Try API call again, with new accessToken 64 | requestObj.headers.Authorization = 'Bearer ' + newToken.accessToken 65 | console.log('Trying again, making API call to: ' + requestObj.url) 66 | request(requestObj, function (err, response) { 67 | // Logic (including error checking) should be continued with new 68 | // err/response objects. 69 | resolve({err, response}) 70 | }) 71 | }, function(err) { 72 | // Error refreshing the tokens 73 | reject(err) 74 | }) 75 | } else { 76 | // No 401, continue! 77 | resolve({err, response}) 78 | } 79 | }) 80 | } 81 | 82 | // Refresh Token should be called if access token expires, or if Intuit 83 | // returns a 401 Unauthorized. 84 | this.refreshTokens = function(session) { 85 | var token = this.getToken(session) 86 | 87 | // Call refresh API 88 | return token.refresh().then(function(newToken) { 89 | // Store the new tokens 90 | tools.saveToken(session, newToken) 91 | return newToken 92 | }) 93 | } 94 | 95 | //A static function to refresh Token with refresh token. Return the token created. 96 | this.refreshTokensWithToken = function(token) { 97 | if(!token) { 98 | return Promise.reject(new Error('Nil Token passed for refreshTokensWithToken')) 99 | } 100 | 101 | request({ 102 | url: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer', 103 | method: 'POST', 104 | headers: { 105 | 'Accept': 'application/json', 106 | 'Authorization': 'Basic ' + tools.basicAuth, 107 | 'Content-Type' : 'application/x-www-form-urlencoded' 108 | }, 109 | form: { 110 | refresh_token: token, 111 | grant_type: 'refresh_token' 112 | } 113 | }, function(err, response) { 114 | if(err) { 115 | console.log(err) 116 | return err 117 | } 118 | 119 | var json = JSON.parse(response.body) 120 | return tools.intuitAuth.createToken( 121 | json.access_token, json.refresh_token, 122 | json.token_type, json.x_refresh_token_expires_in) 123 | }) 124 | } 125 | 126 | this.setScopes = function(flowName) { 127 | authConfig.scopes = config.scopes[flowName] 128 | tools.intuitAuth = new ClientOAuth2(authConfig) 129 | } 130 | 131 | this.containsOpenId = function() { 132 | if(!authConfig.scopes) return false; 133 | return authConfig.scopes.includes('openid') 134 | } 135 | 136 | // Setup OAuth2 Client with values from config.json 137 | this.intuitAuth = new ClientOAuth2(authConfig) 138 | 139 | // Get anti-forgery token to use for state 140 | this.generateAntiForgery = function(session) { 141 | session.secret = csrf.secretSync() 142 | return csrf.create(session.secret) 143 | } 144 | 145 | this.verifyAntiForgery = function(session, token) { 146 | return csrf.verify(session.secret, token) 147 | } 148 | 149 | this.clearToken = function(session) { 150 | session.accessToken = null 151 | session.refreshToken = null 152 | session.tokenType = null 153 | session.data = null 154 | } 155 | 156 | // Save token into session storage 157 | // In a real use-case, this is where tokens would have to be persisted (to a 158 | // a SQL DB, for example). Both access tokens and refresh tokens need to be 159 | // persisted. This should typically be stored against a user / realm ID, as well. 160 | this.saveToken = function(session, token) { 161 | session.accessToken = token.accessToken 162 | session.refreshToken = token.refreshToken 163 | session.tokenType = token.tokenType 164 | session.data = token.data 165 | } 166 | 167 | // Get the token object from session storage 168 | this.getToken = function(session) { 169 | if(!session.accessToken) return null 170 | 171 | return tools.intuitAuth.createToken( 172 | session.accessToken, session.refreshToken, 173 | session.tokenType, session.data 174 | ) 175 | } 176 | 177 | this.refreshEndpoints() 178 | } 179 | 180 | module.exports = new Tools(); 181 | -------------------------------------------------------------------------------- /views/Callout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/oauth2-nodejs/22f1093174db797d01badcfe4cdd42ff3039649f/views/Callout.png -------------------------------------------------------------------------------- /views/Sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/oauth2-nodejs/22f1093174db797d01badcfe4cdd42ff3039649f/views/Sample.png -------------------------------------------------------------------------------- /views/connected.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | OAuth2 Sample App - Intuit 4 | 5 | 6 | 31 | 32 | 33 | Home 34 |

Connected!

35 |

Welcome<% if (locals.givenName) { %>, <%= locals.givenName %><% } %>!

36 |

Would you like to make a sample API call?

37 |
38 | 39 | 40 | 41 |

42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | OAuth2 Sample App - Intuit 4 | 5 | 6 | 7 |

Welcome to the Intuit OAuth2 Sample App!

8 | Before using this app, please make sure you do the following: 9 | 23 |


24 | 25 | 26 | 27 | Sign In With Intuit
28 | 29 | 33 | 34 |


35 | 36 | 37 | Connect To QuickBooks
38 | 39 | 45 | 46 |


47 | 48 | 49 | Get App Now
50 | 51 | Get App Now 52 | 53 |


54 | 55 | 66 | 67 | 68 | 69 | --------------------------------------------------------------------------------