├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── push.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .snyk ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── mocha.opts ├── package.json ├── prettier.config.js ├── sample ├── .env.example ├── .snyk ├── README.md ├── app.js ├── package.json └── public │ ├── css │ └── common.css │ ├── images │ ├── C2QB_green_btn_lg_default.png │ ├── Sample.png │ └── quickbooks_logo_horz.png │ └── index.html ├── src ├── OAuthClient.js ├── access-token │ └── Token.js └── response │ └── AuthResponse.js ├── test ├── AuthResponseTest.js ├── OAuthClientTest.js ├── TokenTest.js └── mocks │ ├── authResponse.json │ ├── bearer-token.json │ ├── errorResponse.json │ ├── expectedValidateIDTokenCall.json │ ├── jwkResponse.json │ ├── makeAPICallResponse.json │ ├── openID-token.json │ ├── pdfResponse.json │ ├── refreshResponse.json │ ├── response.json │ ├── tokenResponse.json │ ├── userInfo.json │ └── validateIdToken.json └── views └── SDK.png /.eslintignore: -------------------------------------------------------------------------------- 1 | src/index.js 2 | sample 3 | .nyc_output 4 | coverage 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "prettier"], 3 | "parserOptions": { 4 | "sourceType": "script" 5 | }, 6 | "rules": { 7 | "strict": ["error", "safe"], 8 | "prefer-object-spread": "off", 9 | "no-param-reassign": "off", 10 | "comma-dangle": [ 11 | "error", 12 | { 13 | "arrays": "always-multiline", 14 | "objects": "always-multiline", 15 | "imports": "always-multiline", 16 | "exports": "always-multiline", 17 | "functions": "never" 18 | } 19 | ], 20 | "no-underscore-dangle": "off", 21 | "no-unused-expressions": "off" 22 | }, 23 | "globals": { 24 | "sinon": true, 25 | "describe": true, 26 | "it": true, 27 | "expect": true, 28 | "test": true, 29 | "require": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [8.x, 10.x, 12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: npm install, build, and test 24 | run: | 25 | npm install 26 | npm test 27 | env: 28 | CI: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gitignore 2 | 3 | .DS_Store 4 | .idea 5 | .idea/ 6 | .nyc_output 7 | .vscode/ 8 | coverage 9 | bower_components 10 | npm-debug.* 11 | node_modules 12 | index.js 13 | src/index.js 14 | sample/node_modules 15 | sample/.env 16 | oauth-jsclient.iml 17 | package-lock.json 18 | yarn.lock 19 | src/logs/* 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | .vscode 4 | 5 | # Samples 6 | sample 7 | test 8 | 9 | # Source files 10 | src/index.js 11 | src/logs 12 | 13 | # Configuration files 14 | .travis.yml 15 | .eslintrc.json 16 | .eslintignore 17 | 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - winston > async > lodash: 8 | patched: '2020-05-01T00:38:24.095Z' 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 10 5 | - 11 6 | - 12 7 | - 14 8 | 9 | before_script: 10 | - npm install 11 | 12 | script: 13 | - npm test 14 | 15 | after_script: make test-coveralls 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.2.0](https://github.com/intuit/oauth-jsclient/tree/4.2.0) 4 | #### Features 5 | - None (includes all minor releases and fixes since 4.1.0) 6 | #### Issues Fixed 7 | - [updated sync-protect package ref, test suite and enabled logs for the sample client](https://github.com/intuit/oauth-jsclient/pull/184) 8 | 9 | ## [4.1.3](https://github.com/intuit/oauth-jsclient/tree/4.1.3) 10 | #### Features 11 | - None 12 | #### Issues Fixed 13 | - [minor fixes OAuthClient.js](https://github.com/intuit/oauth-jsclient/pull/171) 14 | 15 | ## [4.1.2](https://github.com/intuit/oauth-jsclient/tree/4.1.2) 16 | #### Issues Fixed 17 | - [fixed Error converting authResponse to JSON string](https://github.com/intuit/oauth-jsclient/pull/165) 18 | 19 | ## [4.1.1](https://github.com/intuit/oauth-jsclient/tree/4.1.1) 20 | #### Features 21 | - Stop using Popsicle and start using Axios 22 | #### Issues Fixed 23 | - [fix authResponse.json](https://github.com/intuit/oauth-jsclient/pull/160) 24 | 25 | 26 | ## [4.1.0](https://github.com/intuit/oauth-jsclient/tree/4.1.0) 27 | #### Features 28 | - Stop using Popsicle and start using Axios 29 | #### Issues Fixed 30 | - [Introduced Axios replacing Popsicle](https://github.com/intuit/oauth-jsclient/pull/157) 31 | 32 | 33 | ## [4.0.0](https://github.com/intuit/oauth-jsclient/tree/4.0.0) 34 | #### Breaking Changes 35 | - Minimum Node Version >= 10 36 | #### Features 37 | - Supports Minimum Node version 10 and newer ( not backward compatible ) 38 | - Moved lower node versions ( node 8,9, node 7, node 6 to 3.x.x. , 2.x.x and 1.x.x release respectively ) 39 | - node version 8,9 and higher refer to 3.x.x 40 | - node version 7 and higher refer to 2.x.x 41 | - node version 6 and higher refer to 1.x.x 42 | #### Issues Fixed 43 | - [Adding Transport Override for PDF use case](https://github.com/intuit/oauth-jsclient/pull/98) 44 | 45 | #### References 46 | - [PDF Transport](https://github.com/intuit/oauth-jsclient/issues/97) 47 | 48 | 49 | ## [3.0.2](https://github.com/intuit/oauth-jsclient/tree/3.0.2) 50 | #### Features 51 | - [Added support for passing custom authorize URL's](https://github.com/intuit/oauth-jsclient/pull/92) 52 | 53 | 54 | ## [3.0.1](https://github.com/intuit/oauth-jsclient/tree/3.0.1) 55 | #### Issues Fixed 56 | - [`snyk` package as a dependency since the 3.0 version](https://github.com/intuit/oauth-jsclient/issues/88) 57 | 58 | ## [3.0.0](https://github.com/intuit/oauth-jsclient/tree/3.0.0) 59 | #### Breaking Changes 60 | - Minimum Node Version >= 8 LTS 61 | #### Features 62 | - Supports Minimum Node version 8 LTS and newer ( not backward compatible ) 63 | - Moved lower node versions ( node 7, node 6 to 2.x.x and 1.x.x release respectively ) 64 | - node version 6 and lower refer to 1.x.x 65 | - node version 7 and lower refer to 2.x.x 66 | - Enhanced Code Coverage 67 | #### Issues Fixed 68 | - ES Lint issues fixed. 69 | - Vulnerabilities fixed. 70 | - [Error failing to create if response is missing headers](https://github.com/intuit/oauth-jsclient/issues/70) 71 | 72 | ## [2.1.0](https://github.com/intuit/oauth-jsclient/tree/2.1.0) 73 | #### Features 74 | - Accept Full Range of HTTP Success Codes 75 | - Handle not JSON content in response parsing 76 | - Dependency cleanup ( still pending. Opening an issue ) 77 | #### Issues Fixed 78 | - [Accept Full Range of HTTP Success Codes](https://github.com/intuit/oauth-jsclient/pull/78) 79 | - [Fix: handle not JSON content in response parsing](https://github.com/intuit/oauth-jsclient/pull/59) 80 | 81 | ## [2.0.2](https://github.com/intuit/oauth-jsclient/tree/2.0.2) 82 | #### Features 83 | - Improved Code Coverage 84 | - README Corrections 85 | - Fixed npm package issues in 2.0.1 86 | 87 | ## [2.0.1](https://github.com/intuit/oauth-jsclient/tree/2.0.1) 88 | #### Features 89 | - Improved Code Coverage 90 | - README Corrections 91 | 92 | ## [2.0.0](https://github.com/intuit/oauth-jsclient/tree/2.0.0) 93 | #### Breaking Changes 94 | - Minimum Node Version >= 7.0.0 95 | #### Features 96 | - Supports Minimum Node version >=7.0.0 ( not backward compatible ) 97 | - Support for HTTP methods for API calls other than GET 98 | - Enhanced Code Coverage 99 | - ES Lint issues fixed. 100 | #### Issues Fixed 101 | - [Does this library support any HTTP methods for API calls other than GET](https://github.com/intuit/oauth-jsclient/issues/40) 102 | - [Improve code coverage](https://github.com/intuit/oauth-jsclient/issues/39) 103 | - [Fix ESLint Issues](https://github.com/intuit/oauth-jsclient/issues/40) 104 | - [ngrok current version doesn't work](https://github.com/intuit/oauth-jsclient/issues/41) 105 | 106 | ## [1.5.0](https://github.com/intuit/oauth-jsclient/releases/tag/1.5.0) 107 | #### Features 108 | Problem : 109 | - The csrf tokens created did not follow the singleton pattern. 110 | - Occasional use of strict mode keywords made the package usage difficult in ES6 compliant environments. 111 | 112 | Solution : 113 | 114 | - csrf token instance created at the time of instantiating OAuthClient. Singleton JS design pattern adopted. 115 | - Adopted ES6 standardization 116 | - ESLint enabled 117 | #### Issues Fixed 118 | - [Strict mode keywords](https://github.com/intuit/oauth-jsclient/issues/4) 119 | - [csrf update to OAuthClient](https://github.com/intuit/oauth-jsclient/issues/30) 120 | 121 | 122 | ## [1.4.0](https://github.com/intuit/oauth-jsclient/releases/tag/1.4.0) 123 | #### Features 124 | Problem : 125 | - The access-tokens are valid post the revoke() functionality. 126 | 127 | Solution : 128 | - Clear Token Object on invoking revoke() functionality 129 | 130 | #### Issues Fixed 131 | - [isAccessTokenValid() is true after calling revoke()](https://github.com/intuit/oauth-jsclient/issues/28) 132 | 133 | 134 | ## [1.3.0](https://github.com/intuit/oauth-jsclient/releases/tag/1.3.0) 135 | #### Features 136 | - TokenValidation for Revoke functionality Fixed 137 | #### Issues Fixed 138 | - Release Updates 139 | - Revoke token [README.md](https://github.com/intuit/oauth-jsclient#revoke-access_token) 140 | 141 | ## [1.2.0](https://github.com/intuit/oauth-jsclient/releases/tag/1.2.0) 142 | #### Features 143 | - Highly Improved Implementation : setToken functionality 144 | #### Issues Fixed 145 | 1.) the setToken() and the constructor for passing the tokens are handled efficiently now. 146 | 2.) [#20](https://github.com/intuit/oauth-jsclient/pull/20) and [#7](https://github.com/intuit/oauth-jsclient/pull/7) - Fixed 147 | 3.) [#19](https://github.com/intuit/oauth-jsclient/issues/19) - HTTP 4XX Errors handled with more information. 148 | 149 | ## [1.1.3](https://github.com/intuit/oauth-jsclient/releases/tag/1.1.3) 150 | #### Features 151 | - Setting the Token methodology fixed 152 | #### Issues Fixed 153 | - [Error revoking token, this.token.refreshToken is not a function](https://github.com/intuit/oauth-jsclient/issues/16) 154 | 155 | ## [1.1.2](https://github.com/intuit/oauth-jsclient/releases/tag/1.1.2) 156 | #### Features 157 | - Supports Token Setting Functionality + New Scopes added 158 | - New scopes added : 159 | - Payroll: com.intuit.quickbooks.payroll, 160 | - TimeTracking: com.intuit.quickbooks.payroll.timetracking, 161 | - Benefits: com.intuit.quickbooks.payroll.benefits, 162 | #### Issues Fixed 163 | - [typo '/getCompanyInfo' in app.js](https://github.com/intuit/oauth-jsclient/issues/11) 164 | - [Console logging of tokens seems like a bad idea](https://github.com/intuit/oauth-jsclient/issues/13) 165 | 166 | ## [1.1.1](https://github.com/intuit/oauth-jsclient/releases/tag/1.1.1) 167 | 168 | - Rolling Back changes to realmId field on createToken() 169 | 170 | ## [1.1.0](https://github.com/intuit/oauth-jsclient/releases/tag/1.1.0) 171 | #### Features 172 | - Support for passing realmId and id_token using setToken() 173 | #### Issues Fixed 174 | - Support for optionally passing realmId and id_token using setToken() 175 | - Issues fixed for #5 176 | - Issues fixed for #6 177 | - Issues fixed for #7 178 | 179 | ## [1.0.3](https://github.com/intuit/oauth-jsclient/releases/tag/1.0.3) 180 | #### Features 181 | - Support for RefreshUsingToken method 182 | 183 | ## [1.0.2](https://github.com/intuit/oauth-jsclient/releases/tag/1.0.2) 184 | 185 | - Version Release - 1.0.2 186 | 187 | ## [1.0.1](https://github.com/intuit/oauth-jsclient/releases/tag/1.0.1) 188 | 189 | - npm publish patch - 1.0.1 190 | 191 | ## [1.0.0](https://github.com/intuit/oauth-jsclient/releases/tag/1.0.0) 192 | 193 | - First Release - 1.0.0 194 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 2018 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPORTER = spec 2 | test: 3 | @$(MAKE) lint 4 | @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) 5 | @NODE_ENV=test ./node_modules/.bin/mocha -b --reporter $(REPORTER) 6 | 7 | lint: 8 | ./node_modules/.bin/jshint ./lib ./test ./index.js 9 | 10 | test-cov: 11 | $(MAKE) lint 12 | @NODE_ENV=test ./node_modules/.bin/istanbul cover \ 13 | ./node_modules/mocha/bin/_mocha -- -R spec 14 | 15 | test-coveralls: 16 | @NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 17 | 18 | 19 | 20 | .PHONY: test 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![SDK Banner](views/SDK.png)][ss1] 2 | 3 | [![Build Status](https://travis-ci.org/intuit/oauth-jsclient.svg?branch=master)](https://travis-ci.org/intuit/oauth-jsclient?branch=master) 4 | [![NPM Package Version](https://img.shields.io/npm/v/intuit-oauth.svg?style=flat-square)](https://www.npmjs.com/package/intuit-oauth) 5 | [![Coverage Status](https://coveralls.io/repos/github/intuit/oauth-jsclient/badge.svg?branch=master)](https://coveralls.io/github/intuit/oauth-jsclient?branch=master) 6 | [![GitHub contributors](https://img.shields.io/github/contributors/intuit/oauth-jsclient?style=flat-square)](https://github.com/intuit/oauth-jsclient/graphs/contributors) 7 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/intuit/oauth-jsclient/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/intuit/oauth-jsclient/?branch=master) 8 | ![npm](https://img.shields.io/npm/dm/intuit-oauth?style=flat-square) 9 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=shield)](https://github.com/prettier/prettier) 10 | [![Known Vulnerabilities](https://snyk.io/test/github/intuit/oauth-jsclient/badge.svg)](https://snyk.io/test/github/intuit/oauth-jsclient) 11 | 12 | # Intuit OAuth2.0 NodeJS Library 13 | 14 | The OAuth2 Nodejs Client library is meant to work with Intuit's 15 | [OAuth2.0](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0) 16 | and 17 | [OpenID Connect](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/openid-connect) 18 | implementations which conforms to the specifications. 19 | 20 | ## Table of Contents 21 | 22 | - [Intuit OAuth2.0 NodeJS Library](#intuit-oauth20-nodejs-library) 23 | - [Table of Contents](#table-of-contents) 24 | - [Requirements](#requirements) 25 | - [Installation](#installation) 26 | - [Using NodeJS](#using-nodejs) 27 | - [Options :](#options) 28 | - [Usage](#usage) 29 | - [Authorization Code Flow](#authorization-code-flow) 30 | - [Step 1](#step-1) 31 | - [Scopes :](#scopes) 32 | - [Step 2](#step-2) 33 | - [Sample](#sample) 34 | - [Helpers](#helpers) 35 | - [Is AccessToken Valid](#is-accesstoken-valid) 36 | - [Refresh access_token](#refresh-access_token) 37 | - [Refresh access_token by passing the refresh_token explicitly](#refresh-access_token-by-passing-the-refresh_token-explicitly) 38 | - [Revoke access_token](#retrieve-the-token-) 39 | - [Getter / Setter for Token](#getter--setter-for-token) 40 | - [Retrieve the Token :](#retrieve-the-token) 41 | - [Set the Token :](#set-the-token-) 42 | - [Migrate OAuth1.0 Tokens to OAuth2.0](#migrate-oauth10-tokens-to-oauth20) 43 | - [Validate ID Token](#validate-id-token) 44 | - [Make API call](#make-api-call) 45 | - [Auth-Response](#auth-response) 46 | - [Error Logging](#error-logging) 47 | - [FAQ](#faq) 48 | - [Contributing](#contributing) 49 | - [Steps](#steps) 50 | - [Changelog](#changelog) 51 | - [License](#license) 52 | 53 | # Requirements 54 | 55 | The Node.js client library is tested against the `Node 10` and newer versions. 56 | 57 | | Version | Node support | 58 | |----------------------------------------------------------------------------------|-----------------------------------| 59 | | [intuit-oauth@1.x.x](https://github.com/intuit/oauth-jsclient/tree/1.5.0) | Node 6.x or higher | 60 | | [intuit-oauth@2.x.x](https://github.com/intuit/oauth-jsclient/tree/2.0.0) | Node 7.x or higher | 61 | | [intuit-oauth@3.x.x](https://github.com/intuit/oauth-jsclient/tree/3.0.2) | Node 8.x or Node 9.x and higher | 62 | 63 | **Note**: Older node versions are not supported. 64 | 65 | # Installation 66 | 67 | Follow the instructions below to use the library : 68 | 69 | ## Using NodeJS 70 | 71 | 1. Install the NPM package: 72 | 73 | ```sh 74 | npm install intuit-oauth --save 75 | ``` 76 | 77 | 2. Require the Library: 78 | 79 | ```js 80 | const OAuthClient = require('intuit-oauth'); 81 | 82 | const oauthClient = new OAuthClient({ 83 | clientId: '', 84 | clientSecret: '', 85 | environment: 'sandbox' || 'production', 86 | redirectUri: '', 87 | }); 88 | ``` 89 | 90 | ### Options 91 | 92 | - `clientId` - clientID for your app. Required 93 | - `clientSecret` - clientSecret fpor your app. Required 94 | - `environment` - environment for the client. Required 95 | - `sandbox` - for authorizing in sandbox. 96 | - `production` - for authorizing in production. 97 | - `redirectUri` - redirectUri on your app to get the `authorizationCode` from Intuit Servers. Make sure this redirect URI is also added on your app in the [developer portal](https://developer.intuit.com) on the Keys & OAuth tab. Required 98 | - `logging` - by default, logging is disabled i.e `false`. To enable provide`true`. 99 | 100 | # Usage 101 | 102 | We assume that you have a basic understanding about OAuth2.0. If not please read 103 | [API Documentation](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0) 104 | for clear understanding 105 | 106 | ## Authorization Code Flow 107 | 108 | The Authorization Code flow is made up of two parts : 109 | 110 | **Step 1.** Redirect user to `oauthClient.authorizeUri(options)`. 111 | **Step 2.** Parse response uri and get access-token using the function 112 | `oauthClient.createToken(req.url)` which returns a 113 | [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). 114 | 115 | ### Step 1 116 | 117 | ```javascript 118 | // Instance of client 119 | const oauthClient = new OAuthClient({ 120 | clientId: '', 121 | clientSecret: '', 122 | environment: 'sandbox', 123 | redirectUri: '', 124 | }); 125 | 126 | // AuthorizationUri 127 | const authUri = oauthClient.authorizeUri({ 128 | scope: [OAuthClient.scopes.Accounting, OAuthClient.scopes.OpenId], 129 | state: 'testState', 130 | }); // can be an array of multiple scopes ex : {scope:[OAuthClient.scopes.Accounting,OAuthClient.scopes.OpenId]} 131 | 132 | // Redirect the authUri 133 | res.redirect(authUri); 134 | ``` 135 | 136 | ### Scopes 137 | 138 | The available scopes include : 139 | 140 | - `com.intuit.quickbooks.accounting` - for accounting scope include `OAuthClient.scopes.Accounting` 141 | - `com.intuit.quickbooks.payment` - for payment scope include `OAuthClient.scopes.Payment` 142 | - `com.intuit.quickbooks.payroll` - for QuickBooks Payroll API (whitelisted beta apps only) 143 | - `com.intuit.quickbooks.payroll.timetracking` - for QuickBooks Payroll API for for access to 144 | compensation (whitelisted beta apps only) 145 | - `com.intuit.quickbooks.payroll.benefits` - for QuickBooks Payroll API for access to 146 | benefits/pension/deduction (whitelisted beta apps only) 147 | 148 | OpenID Scopes : 149 | 150 | - `openid` - for openID assertion include `OAuthClient.scopes.OpenId` 151 | - `profile` - for profile assertion include `OAuthClient.scopes.Profile` 152 | - `email` - for email assertion include `OAuthClient.scopes.Email` 153 | - `phone` - for phone assertion include `OAuthClient.scopes.Phone` 154 | - `address` - for address assertion include `OAuthClient.scopes.Address` 155 | 156 | ### Step 2 157 | 158 | ```javascript 159 | // Parse the redirect URL for authCode and exchange them for tokens 160 | const parseRedirect = req.url; 161 | 162 | // Exchange the auth code retrieved from the **req.url** on the redirectUri 163 | oauthClient 164 | .createToken(parseRedirect) 165 | .then(function (authResponse) { 166 | console.log('The Token is ' + JSON.stringify(authResponse.getToken())); 167 | }) 168 | .catch(function (e) { 169 | console.error('The error message is :' + e.originalMessage); 170 | console.error(e.intuit_tid); 171 | }); 172 | ``` 173 | 174 | # Sample 175 | 176 | For more clarity, we suggest you take a look at the sample application below : 177 | [sample](https://github.com/intuit/oauth-jsclient/tree/master/sample) 178 | 179 | ## Helpers 180 | 181 | ### Is AccessToken Valid 182 | 183 | You can check if the `access_token` associated with the `oauthClient` is valid ( not expired ) or 184 | not using the helper method. 185 | 186 | ```javascript 187 | if (oauthClient.isAccessTokenValid()) { 188 | console.log('The access_token is valid'); 189 | } 190 | 191 | if (!oauthClient.isAccessTokenValid()) { 192 | oauthClient 193 | .refresh() 194 | .then(function (authResponse) { 195 | console.log('Tokens refreshed : ' + JSON.stringify(authResponse.getToken())); 196 | }) 197 | .catch(function (e) { 198 | console.error('The error message is :' + e.originalMessage); 199 | console.error(e.intuit_tid); 200 | }); 201 | } 202 | ``` 203 | 204 | \*\* Note: If the access_token is not valid, you can call the client's `refresh()` method to refresh 205 | the tokens for you as shown below 206 | 207 | ### Refresh access_token 208 | 209 | Access tokens are valid for 3600 seconds (one hour), after which time you need to get a fresh one 210 | using the latest refresh_token returned to you from the previous request. When you request a fresh 211 | access_token, always use the refresh token returned in the most recent token_endpoint response. Your 212 | previous refresh tokens expire 24 hours after you receive a new one. 213 | 214 | ```javascript 215 | oauthClient 216 | .refresh() 217 | .then(function (authResponse) { 218 | console.log('Tokens refreshed : ' + JSON.stringify(authResponse.getToken())); 219 | }) 220 | .catch(function (e) { 221 | console.error('The error message is :' + e.originalMessage); 222 | console.error(e.intuit_tid); 223 | }); 224 | ``` 225 | 226 | ### Refresh access_token by passing the refresh_token explicitly 227 | 228 | You can call the below helper method to refresh tokens by explictly passing the refresh_token. 229 | \*\*Note : `refresh_token` should be of the type `string` 230 | 231 | ```javascript 232 | oauthClient 233 | .refreshUsingToken('') 234 | .then(function (authResponse) { 235 | console.log('Tokens refreshed : ' + JSON.stringify(authResponse.getToken())); 236 | }) 237 | .catch(function (e) { 238 | console.error('The error message is :' + e.originalMessage); 239 | console.error(e.intuit_tid); 240 | }); 241 | ``` 242 | 243 | ### Revoke access_token 244 | 245 | When you no longer need the access_token, you could use the below helper method to revoke the 246 | tokens. 247 | 248 | ```javascript 249 | oauthClient 250 | .revoke() 251 | .then(function (authResponse) { 252 | console.log('Tokens revoked : ' + JSON.stringify(authResponse.json)); 253 | }) 254 | .catch(function (e) { 255 | console.error('The error message is :' + e.originalMessage); 256 | console.error(e.intuit_tid); 257 | }); 258 | ``` 259 | 260 | Alternatively you can also pass `access_token` or `refresh_token` to this helper method using the 261 | `params` object: refer to - [Getter / Setter for Token](#getter--setter-for-token) section to know 262 | how to retrieve the `token` object 263 | 264 | ```javascript 265 | oauthClient 266 | .revoke(params) 267 | .then(function (authResponse) { 268 | console.log('Tokens revoked : ' + JSON.stringify(authResponse.json)); 269 | }) 270 | .catch(function (e) { 271 | console.error('The error message is :' + e.originalMessage); 272 | console.error(e.intuit_tid); 273 | }); 274 | ``` 275 | 276 | ** Note ** : `params` is the Token JSON object as shown below : ( _If you do not pass the `params` 277 | then the token object of the client would be considered._) 278 | 279 | ``` 280 | { 281 | "token_type": "bearer", 282 | "expires_in": 3600, 283 | "refresh_token":"", 284 | "x_refresh_token_expires_in":15552000, 285 | "access_token":"", 286 | "createdAt": "(Optional Default = Date.now()) from the unix epoch" 287 | 288 | } 289 | ``` 290 | 291 | ** Note ** : 292 | 293 | ### Getter / Setter for Token 294 | 295 | You can call the below methods to set and get the tokens using the `oauthClient` instance: 296 | 297 | #### Retrieve the Token : 298 | 299 | ```javascript 300 | // To get the tokens 301 | let authToken = oauthClient.getToken().getToken(); 302 | 303 | `OR`; 304 | 305 | let authToken = oauthClient.token.getToken(); 306 | ``` 307 | 308 | #### Set the Token : 309 | 310 | ```javascript 311 | // To Set the retrieved tokens explicitly using Token Object but the same instance 312 | oauthClient.setToken(authToken); 313 | 314 | OR; 315 | 316 | // To set the retrieved tokens using a new client instance 317 | const oauthClient = new OAuthClient({ 318 | clientId: '', 319 | clientSecret: '', 320 | environment: 'sandbox', 321 | redirectUri: '', 322 | token: authToken, 323 | }); 324 | ``` 325 | 326 | The authToken parameters are as follows: 327 | 328 | ``` 329 | { 330 | token_type: '', 331 | access_token: '', 332 | expires_in: ' Seconds', 333 | refresh_token: '', 334 | x_refresh_token_expires_in: ' Seconds', 335 | id_token: "(Optional Default = '') ", 336 | createdAt: '(Optional Default = Date.now()) from the unix epoch' 337 | } 338 | ``` 339 | 340 | **Note** : 341 | The OAuth Client library converts the accessToken and refreshToken expiry time to `TimeStamp`. If 342 | you are setting a stored token, please pass in the `createdAt` for accurate experiations. 343 | 344 | ```javascript 345 | oauthClient.setToken(authToken); 346 | ``` 347 | 348 | ### Migrate OAuth1.0 Tokens to OAuth2.0 349 | 350 | You can call the below method to migrate the bearer / refresh tokens from OAuth1.0 to OAuth2.0. You 351 | 352 | ```javascript 353 | // Fill in the params object ( argument to the migrate function ) 354 | 355 | let params = { 356 | oauth_consumer_key: '', 357 | oauth_consumer_secret: '', 358 | oauth_signature_method: 'HMAC-SHA1', 359 | oauth_timestamp: Math.round(new Date().getTime() / 1000), 360 | oauth_nonce: 'nonce', 361 | oauth_version: '1.0', 362 | access_token: '', 363 | access_secret: '', 364 | scope: [OAuthClient.scopes.Accounting], 365 | }; 366 | 367 | oauthClient 368 | .migrate(params) 369 | .then(function (response) { 370 | console.log('The response is ' + JSON.stringify(response)); 371 | }) 372 | .catch(function (e) { 373 | console.log('The error is ' + e.message); 374 | }); 375 | ``` 376 | 377 | ### Validate ID Token 378 | 379 | You can validate the ID token obtained from `Intuit Authorization Server` as shown below : 380 | 381 | ```javascript 382 | oauthClient 383 | .validateIdToken() 384 | .then(function (response) { 385 | console.log('Is my ID token validated : ' + response); 386 | }) 387 | .catch(function (e) { 388 | console.log('The error is ' + JSON.stringify(e)); 389 | }); 390 | 391 | // Is my ID token validated : true 392 | ``` 393 | 394 | The client validates the ID Token and returns boolean `true` if validates successfully else it would 395 | throw an exception. 396 | 397 | ### Make API Call 398 | 399 | You can make API call using the token generated from the client as shown below : 400 | 401 | ```javascript 402 | // Body sample from API explorer examples 403 | const body = { 404 | TrackQtyOnHand: true, 405 | Name: 'Garden Supplies', 406 | QtyOnHand: 10, 407 | InvStartDate: '2015-01-01', 408 | Type: 'Inventory', 409 | IncomeAccountRef: { 410 | name: 'Sales of Product Income', 411 | value: '79', 412 | }, 413 | AssetAccountRef: { 414 | name: 'Inventory Asset', 415 | value: '81', 416 | }, 417 | ExpenseAccountRef: { 418 | name: 'Cost of Goods Sold', 419 | value: '80', 420 | }, 421 | }; 422 | 423 | oauthClient 424 | .makeApiCall({ 425 | url: 'https://sandbox-quickbooks.api.intuit.com/v3/company/1234/item', 426 | method: 'POST', 427 | headers: { 428 | 'Content-Type': 'application/json', 429 | }, 430 | body: JSON.stringify(body), 431 | }) 432 | .then(function (response) { 433 | console.log('The API response is : ' + response); 434 | }) 435 | .catch(function (e) { 436 | console.log('The error is ' + JSON.stringify(e)); 437 | }); 438 | ``` 439 | 440 | The client validates the ID Token and returns boolean `true` if validates successfully else it would 441 | throw an exception. 442 | 443 | #### Support for PDF format 444 | In order to save the PDF generated from the APIs properly, the correct transport type should be passed into the `makeAPI()`.Below is an example of the same: 445 | ``` 446 | .makeApiCall({ url: `${url}v3/company/${companyID}/invoice/${invoiceNumber}/pdf?minorversion=59` , headers:{'Content-Type': 'application/pdf','Accept':'application/pdf'}, transport: popsicle.createTransport({type: 'buffer'})}) 447 | ``` 448 | The response is an actual buffer( binary BLOB) which could then be saved to the file. 449 | 450 | ### Auth-Response 451 | 452 | The response provided by the client is a wrapped response of the below items which is what we call 453 | authResponse, lets see how it looks like: 454 | 455 | ```text 456 | 457 | 1. response // response from `HTTP Client` used by library 458 | 2. token // instance of `Token` Object 459 | 3. body // res.body in `text` 460 | 4. json // res.body in `JSON` 461 | 5. intuit_tid // `intuit-tid` from response headers 462 | 463 | ``` 464 | 465 | A sample `AuthResponse` object would look similar to : 466 | 467 | ```json 468 | { 469 | "token": { 470 | "realmId": "", 471 | "token_type": "bearer", 472 | "access_token": "", 473 | "refresh_token": "", 474 | "expires_in": 3600, 475 | "x_refresh_token_expires_in": 8726400, 476 | "id_token": "", 477 | "latency": 60000 478 | }, 479 | "response": { 480 | "url": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer", 481 | "headers": { 482 | "content-type": "application/json;charset=UTF-8", 483 | "content-length": "61", 484 | "connection": "close", 485 | "server": "nginx", 486 | "strict-transport-security": "max-age=15552000", 487 | "intuit_tid": "1234-1234-1234-123", 488 | "cache-control": "no-cache, no-store", 489 | "pragma": "no-cache" 490 | }, 491 | "body": "{\"id_token\":\"\",\"expires_in\":3600,\"token_type\":\"bearer\",\"x_refresh_token_expires_in\":8726400,\"refresh_token\":\"\",\"access_token\":\"\"}", 492 | "status": 200, 493 | "statusText": "OK" 494 | }, 495 | "body": "{\"id_token\":\"\",\"expires_in\":3600,\"token_type\":\"bearer\",\"x_refresh_token_expires_in\":8726400,\"refresh_token\":\"\",\"access_token\":\"\"}", 496 | "json": { 497 | "access_token": "", 498 | "refresh_token": "", 499 | "token_type": "bearer", 500 | "expires_in": "3600", 501 | "x_refresh_token_expires_in": "8726400", 502 | "id_token": "" 503 | }, 504 | "intuit_tid": "4245c696-3710-1548-d1e0-d85918e22ebe" 505 | } 506 | ``` 507 | 508 | You can use the below helper methods to make full use of the Auth Response Object : 509 | 510 | ```javascript 511 | oauthClient.createToken(parseRedirect).then(function (authResponse) { 512 | console.log('The Token in JSON is ' + JSON.stringify(authResponse.json)); 513 | let status = authResponse.status(); 514 | let body = authResponse.text(); 515 | let jsonResponse = authResponse.json; 516 | let intuit_tid = authResponse.get_intuit_tid(); 517 | }); 518 | ``` 519 | 520 | ### Error Logging 521 | 522 | By default the logging is `disabled` i.e set to `false`. However, to enable logging, pass 523 | `logging=true` when you create the `oauthClient` instance : 524 | 525 | ```javascript 526 | const oauthClient = new OAuthClient({ 527 | clientId: '', 528 | clientSecret: '', 529 | environment: 'sandbox', 530 | redirectUri: '', 531 | logging: true, 532 | }); 533 | ``` 534 | 535 | The logs would be captured under the directory `/logs/oAuthClient-log.log` 536 | 537 | Whenever there is an error, the library throws an exception and you can use the below helper methods 538 | to retrieve more information : 539 | 540 | ```javascript 541 | oauthClient.createToken(parseRedirect).catch(function (error) { 542 | console.log(error); 543 | }); 544 | 545 | /** 546 | * This is how the Error Object Looks : 547 | { 548 | "originalMessage":"Response has an Error", 549 | "error":"invalid_grant", 550 | "error_description":"Token invalid", 551 | "intuit_tid":"4245c696-3710-1548-d1e0-d85918e22ebe" 552 | } 553 | */ 554 | ``` 555 | 556 | ## FAQ 557 | 558 | You can refer to our [FAQ](https://github.com/intuit/oauth-jsclient/wiki/FAQ) if you have any 559 | questions. 560 | 561 | ## Contributing 562 | 563 | - You are welcome to send a PR to `develop` branch. 564 | - The `master` branch will always point to the latest published version. 565 | - The `develop` branch will contain the latest development/testing changes. 566 | 567 | ### Steps 568 | 569 | - Fork and clone the repository (`develop` branch). 570 | - Run `npm install` for dependencies. 571 | - Run `npm test` to execute all specs. 572 | 573 | ## Changelog 574 | 575 | See the changelog [here](https://github.com/intuit/oauth-jsclient/blob/master/CHANGELOG.md) 576 | 577 | ## License 578 | 579 | Intuit `oauth-jsclient` is licensed under the 580 | [Apache License, Version 2.0](https://github.com/intuit/oauth-jsclient/blob/master/LICENSE) 581 | 582 | [ss1]: https://help.developer.intuit.com/s/SDKFeedback?cid=1120 583 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | /test/*.js 3 | --ui bdd 4 | --slow 20 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intuit-oauth", 3 | "version": "4.2.0", 4 | "description": "Intuit Node.js client for OAuth2.0 and OpenIDConnect", 5 | "main": "./src/OAuthClient.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "karma": "karma start karma.conf.js", 9 | "test": "nyc mocha", 10 | "snyk": "snyk test", 11 | "lint": "eslint .", 12 | "fix": "eslint . --fix", 13 | "posttest": "nyc check-coverage", 14 | "test-watch": "mocha --watch --reporter=spec", 15 | "test-debug": "mocha --inspect-brk --watch test", 16 | "show-coverage": "npm test; open -a 'Google Chrome' coverage/index.html", 17 | "clean-install": "rm -rf node_modules && npm install", 18 | "snyk-protect": "snyk-protect", 19 | "prepublish": "npm run snyk-protect" 20 | }, 21 | "keywords": [ 22 | "intuit-oauth", 23 | "intuit-oauth-nodejs", 24 | "intuit-nodejs", 25 | "oauth2.0", 26 | "openid", 27 | "openidConnect", 28 | "quickbooks-accounting", 29 | "quickbooks-payment" 30 | ], 31 | "nyc": { 32 | "exclude": [ 33 | "node_modules", 34 | "bin", 35 | "coverage", 36 | ".nyc_output", 37 | "sample", 38 | "sample/node_modules" 39 | ], 40 | "check-coverage": true, 41 | "lines": 95, 42 | "statements": 95, 43 | "functions": 90, 44 | "branches": 85, 45 | "reporter": [ 46 | "lcov", 47 | "text", 48 | "text-summary", 49 | "html", 50 | "json" 51 | ] 52 | }, 53 | "engines": { 54 | "node": ">=10" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "https://github.com/intuit/oauth-jsclient.git" 59 | }, 60 | "author": { 61 | "name": "Anil Kumar", 62 | "email": "anil_kumar3@intuit.com" 63 | }, 64 | "license": "Apache-2.0", 65 | "bugs": { 66 | "url": "https://github.com/intuit/oauth-jsclient/issues" 67 | }, 68 | "homepage": "https://github.com/intuit/oauth-jsclient", 69 | "dependencies": { 70 | "atob": "2.1.2", 71 | "axios": "^1.5.1", 72 | "csrf": "^3.0.4", 73 | "jsonwebtoken": "^9.0.2", 74 | "query-string": "^6.12.1", 75 | "rsa-pem-from-mod-exp": "^0.8.4", 76 | "winston": "^3.1.0" 77 | }, 78 | "devDependencies": { 79 | "btoa": "^1.2.1", 80 | "chai": "^4.1.2", 81 | "chai-as-promised": "^7.1.1", 82 | "eslint": "^6.8.0", 83 | "eslint-config-airbnb-base": "^14.1.0", 84 | "eslint-config-prettier": "^6.11.0", 85 | "eslint-plugin-import": "^2.20.2", 86 | "mocha": "^10.2.0", 87 | "nock": "^9.2.3", 88 | "nyc": "^15.0.1", 89 | "prettier": "^2.0.5", 90 | "sinon": "^9.0.2", 91 | "@snyk/protect": "^1.657.0" 92 | }, 93 | "snyk": true 94 | } 95 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | arrowParens: 'always', 5 | printWidth: 100, 6 | proseWrap: 'always', 7 | }; 8 | -------------------------------------------------------------------------------- /sample/.env.example: -------------------------------------------------------------------------------- 1 | # Environment Variables. 2 | 3 | 4 | PORT= 5 | NGROK_ENABLED= true 6 | -------------------------------------------------------------------------------- /sample/.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - intuit-oauth > winston > async > lodash: 8 | patched: '2020-05-01T06:23:47.085Z' 9 | - ngrok > request-promise-native > request-promise-core > lodash: 10 | patched: '2020-05-01T06:23:47.085Z' 11 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Sample Banner](./public/images/Sample.png)][ss1] 3 | 4 | Intuit OAuth2.0 Sample - NodeJS 5 | ========================================================== 6 | 7 | ## Overview 8 | 9 | This is a `sample` app built using Node.js and Express Framework to showcase how to Authorize and Authenticate using Intuit's OAuth2.0 Client library. 10 | 11 | ## Installation 12 | 13 | ### Requirements 14 | 15 | * [Node.js](http://nodejs.org) >= 7.0.0 16 | * [Intuit Developer](https://developer.intuit.com) Account 17 | 18 | ### Via Github Repo (Recommended) 19 | 20 | ```bash 21 | $ cd sample 22 | $ npm install 23 | ``` 24 | 25 | ## Configuration 26 | 27 | Copy the contents from `.env.example` to `.env` within the sample directory: 28 | ```bash 29 | $ cp .env.example .env 30 | ``` 31 | Edit the `.env` file to add your: 32 | 33 | 34 | * **PORT:(optional)** Optional port number for the app to be served 35 | * **NGROK_ENABLED:(optional)** By default it is set to `false`. If you want to serve the Sample App over HTTPS ( which is mandatory if you want to test this app using Production Credentials), set the variable to `true` 36 | 37 | 38 | 39 | ### TLS / SSL (**optional**) 40 | 41 | If you want your enpoint to be exposed over the internet. The easiest way to do that while you are still developing your code locally is to use [ngrok](https://ngrok.com/). 42 | 43 | You dont have to worry about installing ngrok. The sample application does that for you. 44 | 1. Just set `NGROK_ENABLED` = `true` in `.env` 45 | 46 | 47 | ## Usage 48 | 49 | ```bash 50 | $ npm start 51 | ``` 52 | 53 | ### Without ngrok (if you are using localhost i.e `NGROK_ENABLED`=`false` in `.env`) 54 | You will see an URL as below: 55 | ```bash 56 | 💳 Step 1 : Paste this URL in your browser : http://localhost:8000 57 | 💳 Step 2 : Copy and Paste the clientId and clientSecret from : https://developer.intuit.com 58 | 💳 Step 3 : Copy Paste this callback URL into `redirectURI` : http://localhost:8000/callback 59 | 💻 Step 4 : Make Sure this redirect URI is also listed under the Redirect URIs on your app in : https://developer.intuit.com 60 | ``` 61 | 62 | ### With ngrok (if you are using ngrok i.e `NGROK_ENABLED`=`true` in `.env`) 63 | 64 | Your will see an URL as below : 65 | ```bash 66 | 💳 Step 1 : Paste this URL in your browser : https://9b4ee833.ngrok.io 67 | 💳 Step 2 : Copy and Paste the clientId and clientSecret from : https://developer.intuit.com 68 | 💳 Step 3 : Copy Paste this callback URL into `redirectURI` : https://9b4ee833.ngrok.io/callback 69 | 💻 Step 4 : Make Sure this redirect URI is also listed under the Redirect URIs on your app in : https://developer.intuit.com 70 | ``` 71 | 72 | Click on the URL and follow through the instructions given in the sample app. 73 | 74 | 75 | ## Links 76 | 77 | Project Repo 78 | 79 | * https://github.com/intuit/oauth-jsclient 80 | 81 | Intuit OAuth2.0 API Reference 82 | 83 | * https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0 84 | 85 | Intuit OAuth2.0 Playground 86 | 87 | * https://developer.intuit.com/app/developer/playground 88 | 89 | ## Contributions 90 | 91 | Any reports of problems, comments or suggestions are most welcome. 92 | 93 | Please report these on [Issue Tracker in Github](https://github.com/intuit/oauth-jsclient/issues). 94 | 95 | 96 | [ss1]: https://help.developer.intuit.com/s/samplefeedback?cid=9010&repoName=Intuit-OAuth2.0-Sample-NodeJS -------------------------------------------------------------------------------- /sample/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config(); 4 | 5 | /** 6 | * Require the dependencies 7 | * @type {*|createApplication} 8 | */ 9 | const express = require('express'); 10 | 11 | const app = express(); 12 | const path = require('path'); 13 | const OAuthClient = require('intuit-oauth'); 14 | const bodyParser = require('body-parser'); 15 | const ngrok = process.env.NGROK_ENABLED === 'true' ? require('ngrok') : null; 16 | 17 | /** 18 | * Configure View and Handlebars 19 | */ 20 | app.use(bodyParser.urlencoded({ extended: true })); 21 | app.use(express.static(path.join(__dirname, '/public'))); 22 | app.engine('html', require('ejs').renderFile); 23 | 24 | app.set('view engine', 'html'); 25 | app.use(bodyParser.json()); 26 | 27 | const urlencodedParser = bodyParser.urlencoded({ extended: false }); 28 | 29 | /** 30 | * App Variables 31 | * @type {null} 32 | */ 33 | let oauth2_token_json = null; 34 | let redirectUri = ''; 35 | 36 | /** 37 | * Instantiate new Client 38 | * @type {OAuthClient} 39 | */ 40 | 41 | let oauthClient = null; 42 | 43 | /** 44 | * Home Route 45 | */ 46 | app.get('/', function (req, res) { 47 | res.render('index'); 48 | }); 49 | 50 | /** 51 | * Get the AuthorizeUri 52 | */ 53 | app.get('/authUri', urlencodedParser, function (req, res) { 54 | 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, 59 | logging: true, //NOTE: a "logs" folder will be created/used in the current working directory, this will have oAuthClient-log.log 60 | }); 61 | 62 | const authUri = oauthClient.authorizeUri({ 63 | scope: [OAuthClient.scopes.Accounting, OAuthClient.scopes.OpenId, OAuthClient.scopes.Profile, OAuthClient.scopes.Email], 64 | state: 'intuit-test', 65 | }); 66 | res.send(authUri); 67 | }); 68 | 69 | /** 70 | * Handle the callback to extract the `Auth Code` and exchange them for `Bearer-Tokens` 71 | */ 72 | app.get('/callback', function (req, res) { 73 | oauthClient 74 | .createToken(req.url) 75 | .then(function (authResponse) { 76 | oauth2_token_json = JSON.stringify(authResponse.json, null, 2); 77 | }) 78 | .catch(function (e) { 79 | console.error(e); 80 | }); 81 | 82 | res.send(''); 83 | }); 84 | 85 | /** 86 | * Display the token : CAUTION : JUST for sample purposes 87 | */ 88 | app.get('/retrieveToken', function (req, res) { 89 | res.send(oauth2_token_json); 90 | }); 91 | 92 | /** 93 | * Refresh the access-token 94 | */ 95 | app.get('/refreshAccessToken', function (req, res) { 96 | oauthClient 97 | .refresh() 98 | .then(function (authResponse) { 99 | console.log(`\n The Refresh Token is ${JSON.stringify(authResponse.json)}`); 100 | oauth2_token_json = JSON.stringify(authResponse.json, null, 2); 101 | res.send(oauth2_token_json); 102 | }) 103 | .catch(function (e) { 104 | console.error(e); 105 | }); 106 | }); 107 | 108 | /** 109 | * getCompanyInfo () 110 | */ 111 | app.get('/getCompanyInfo', function (req, res) { 112 | const companyID = oauthClient.getToken().realmId; 113 | 114 | const url = 115 | oauthClient.environment == 'sandbox' 116 | ? OAuthClient.environment.sandbox 117 | : OAuthClient.environment.production; 118 | 119 | oauthClient 120 | .makeApiCall({ url: `${url}v3/company/${companyID}/companyinfo/${companyID}` }) 121 | .then(function (authResponse) { 122 | console.log(`\n The response for API call is :${JSON.stringify(authResponse.json)}`); 123 | res.send(authResponse.json); 124 | }) 125 | .catch(function (e) { 126 | console.error(e); 127 | }); 128 | }); 129 | 130 | /** 131 | * disconnect () 132 | */ 133 | app.get('/disconnect', function (req, res) { 134 | console.log('The disconnect called '); 135 | const authUri = oauthClient.authorizeUri({ 136 | scope: [OAuthClient.scopes.OpenId, OAuthClient.scopes.Email], 137 | state: 'intuit-test', 138 | }); 139 | res.redirect(authUri); 140 | }); 141 | 142 | /** 143 | * Start server on HTTP (will use ngrok for HTTPS forwarding) 144 | */ 145 | const server = app.listen(process.env.PORT || 8000, () => { 146 | console.log(`💻 Server listening on port ${server.address().port}`); 147 | if (!ngrok) { 148 | redirectUri = `${server.address().port}` + '/callback'; 149 | console.log( 150 | `💳 Step 1 : Paste this URL in your browser : ` + 151 | 'http://localhost:' + 152 | `${server.address().port}`, 153 | ); 154 | console.log( 155 | '💳 Step 2 : Copy and Paste the clientId and clientSecret from : https://developer.intuit.com', 156 | ); 157 | console.log( 158 | `💳 Step 3 : Copy Paste this callback URL into redirectURI :` + 159 | 'http://localhost:' + 160 | `${server.address().port}` + 161 | '/callback', 162 | ); 163 | console.log( 164 | `💻 Step 4 : Make Sure this redirect URI is also listed under the Redirect URIs on your app in : https://developer.intuit.com`, 165 | ); 166 | } 167 | }); 168 | 169 | /** 170 | * Optional : If NGROK is enabled 171 | */ 172 | if (ngrok) { 173 | console.log('NGROK Enabled'); 174 | ngrok 175 | .connect({ addr: process.env.PORT || 8000 }) 176 | .then((url) => { 177 | redirectUri = `${url}/callback`; 178 | console.log(`💳 Step 1 : Paste this URL in your browser : ${url}`); 179 | console.log( 180 | '💳 Step 2 : Copy and Paste the clientId and clientSecret from : https://developer.intuit.com', 181 | ); 182 | console.log(`💳 Step 3 : Copy Paste this callback URL into redirectURI : ${redirectUri}`); 183 | console.log( 184 | `💻 Step 4 : Make Sure this redirect URI is also listed under the Redirect URIs on your app in : https://developer.intuit.com`, 185 | ); 186 | }) 187 | .catch(() => { 188 | process.exit(1); 189 | }); 190 | } 191 | -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intuit-nodejsclient", 3 | "version": "1.0.0", 4 | "description": "A sample NodeJs application to demonstrate the use of the client OAuth library", 5 | "scripts": { 6 | "start": "node app", 7 | "test": "./node_modules/mocha/bin/mocha test/**/*-test.js --reporter spec", 8 | "snyk-protect": "snyk-protect", 9 | "prepublish": "npm run snyk-protect" 10 | }, 11 | "author": "anil_kumar3@intuit.com", 12 | "license": "APACHE", 13 | "homepage": "https://github.intuit.com/abisalehalliprasan/oauth-jsclient", 14 | "dependencies": { 15 | "body-parser": "latest", 16 | "dotenv": "^5.0.1", 17 | "ejs": "^3.1.9", 18 | "express": "^4.14.0", 19 | "express-session": "^1.14.2", 20 | "intuit-oauth": "^4.1.0", 21 | "ngrok": "^5.0.0-beta.2", 22 | "path": "^0.12.7" 23 | }, 24 | "devDependencies": { 25 | "@snyk/protect": "^1.657.0" 26 | }, 27 | "snyk": true 28 | } 29 | -------------------------------------------------------------------------------- /sample/public/css/common.css: -------------------------------------------------------------------------------- 1 | #headerLogo { 2 | display: inline-block; 3 | width: 200px; 4 | height: 60px; 5 | } 6 | 7 | #csvLogo { 8 | width: 50px; 9 | height: 50px; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /sample/public/images/C2QB_green_btn_lg_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/oauth-jsclient/28182fa8c242e243c3b8ef4f19c526bfda43675a/sample/public/images/C2QB_green_btn_lg_default.png -------------------------------------------------------------------------------- /sample/public/images/Sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/oauth-jsclient/28182fa8c242e243c3b8ef4f19c526bfda43675a/sample/public/images/Sample.png -------------------------------------------------------------------------------- /sample/public/images/quickbooks_logo_horz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/oauth-jsclient/28182fa8c242e243c3b8ef4f19c526bfda43675a/sample/public/images/quickbooks_logo_horz.png -------------------------------------------------------------------------------- /sample/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |

18 | 19 | 20 | 21 |

22 |
23 |
24 |

intuit-nodejsclient sample application

25 |
26 |
27 |

OAuth2.0

( Please enter the client credentials below )


28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 43 |
44 |
45 | 46 |
47 |
48 |
49 |

Now click the Connect to QuickBooks button below.

50 |

 51 |     
 52 |     
 53 |     
 54 |     
55 | 56 |

Make an API call

( Please refer to our API Explorer )

57 |

If there is no access token or the access token is invalid, click either the Connect to QucikBooks or Sign with Intuit button above.

58 |

 59 |     
 60 | 
 61 |     
62 | 63 |

More info:

64 | 69 |
70 |

71 | © 2018 Intuit™, Inc. All rights reserved. Intuit and QuickBooks are registered trademarks of Intuit Inc. 72 |

73 | 74 |
75 | 76 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /src/OAuthClient.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable camelcase */ 3 | /** 4 | 5 | Copyright (c) 2018 Intuit 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | */ 20 | 21 | /** 22 | * @namespace OAuthClient 23 | */ 24 | 25 | 'use strict'; 26 | 27 | const atob = require('atob'); 28 | const Csrf = require('csrf'); 29 | const queryString = require('query-string'); 30 | const axios = require('axios'); 31 | const os = require('os'); 32 | const winston = require('winston'); 33 | const path = require('path'); 34 | const fs = require('fs'); 35 | const jwt = require('jsonwebtoken'); 36 | const AuthResponse = require('./response/AuthResponse'); 37 | const version = require('../package.json'); 38 | const Token = require('./access-token/Token'); 39 | 40 | /** 41 | * @constructor 42 | * @param {string} config.environment 43 | * @param {string} config.appSecret 44 | * @param {string} config.appKey 45 | * @param {string} [config.cachePrefix] 46 | */ 47 | function OAuthClient(config) { 48 | this.environment = config.environment; 49 | this.clientId = config.clientId; 50 | this.clientSecret = config.clientSecret; 51 | this.redirectUri = config.redirectUri; 52 | this.token = new Token(config.token); 53 | this.logging = !!( 54 | Object.prototype.hasOwnProperty.call(config, 'logging') && config.logging === true 55 | ); 56 | this.logger = null; 57 | this.state = new Csrf(); 58 | 59 | if (this.logging) { 60 | const dir = './logs'; 61 | if (!fs.existsSync(dir)) { 62 | fs.mkdirSync(dir); 63 | } 64 | this.logger = winston.createLogger({ 65 | level: 'info', 66 | format: winston.format.combine( 67 | winston.format.timestamp(), 68 | winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), 69 | ), 70 | transports: [ 71 | new winston.transports.File({ 72 | filename: path.join(dir, 'oAuthClient-log.log'), 73 | }), 74 | ], 75 | }); 76 | } 77 | } 78 | 79 | OAuthClient.cacheId = 'cacheID'; 80 | OAuthClient.authorizeEndpoint = 'https://appcenter.intuit.com/connect/oauth2'; 81 | OAuthClient.tokenEndpoint = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer'; 82 | OAuthClient.revokeEndpoint = 'https://developer.api.intuit.com/v2/oauth2/tokens/revoke'; 83 | OAuthClient.userinfo_endpoint_production = 84 | 'https://accounts.platform.intuit.com/v1/openid_connect/userinfo'; 85 | OAuthClient.userinfo_endpoint_sandbox = 86 | 'https://sandbox-accounts.platform.intuit.com/v1/openid_connect/userinfo'; 87 | OAuthClient.migrate_sandbox = 'https://developer-sandbox.api.intuit.com/v2/oauth2/tokens/migrate'; 88 | OAuthClient.migrate_production = 'https://developer.api.intuit.com/v2/oauth2/tokens/migrate'; 89 | OAuthClient.environment = { 90 | sandbox: 'https://sandbox-quickbooks.api.intuit.com/', 91 | production: 'https://quickbooks.api.intuit.com/', 92 | }; 93 | OAuthClient.jwks_uri = 'https://oauth.platform.intuit.com/op/v1/jwks'; 94 | OAuthClient.scopes = { 95 | Accounting: 'com.intuit.quickbooks.accounting', 96 | Payment: 'com.intuit.quickbooks.payment', 97 | Payroll: 'com.intuit.quickbooks.payroll', 98 | TimeTracking: 'com.intuit.quickbooks.payroll.timetracking', 99 | Benefits: 'com.intuit.quickbooks.payroll.benefits', 100 | Profile: 'profile', 101 | Email: 'email', 102 | Phone: 'phone', 103 | Address: 'address', 104 | OpenId: 'openid', 105 | Intuit_name: 'intuit_name', 106 | }; 107 | OAuthClient.user_agent = `Intuit-OAuthClient-JS_${ 108 | version.version 109 | }_${os.type()}_${os.release()}_${os.platform()}`; 110 | 111 | OAuthClient.prototype.setAuthorizeURLs = function setAuthorizeURLs(params) { 112 | // check if the customURL's are passed correctly 113 | if (!params) { 114 | throw new Error("Provide the custom authorize URL's"); 115 | } 116 | OAuthClient.authorizeEndpoint = params.authorizeEndpoint; 117 | OAuthClient.tokenEndpoint = params.tokenEndpoint; 118 | OAuthClient.revokeEndpoint = params.revokeEndpoint; 119 | this.environment === 'sandbox' 120 | ? (OAuthClient.userinfo_endpoint_sandbox = params.userInfoEndpoint) 121 | : (OAuthClient.userinfo_endpoint_production = params.userInfoEndpoint); 122 | 123 | return this; 124 | }; 125 | 126 | /** 127 | * Redirect User to Authorization Page 128 | * * 129 | * @param params 130 | * @returns {string} authorize Uri 131 | */ 132 | OAuthClient.prototype.authorizeUri = function authorizeUri(params) { 133 | params = params || {}; 134 | 135 | // check if the scopes is provided 136 | if (!params.scope) throw new Error('Provide the scopes'); 137 | 138 | const authUri = `${OAuthClient.authorizeEndpoint}?${queryString.stringify({ 139 | response_type: 'code', 140 | redirect_uri: this.redirectUri, 141 | client_id: this.clientId, 142 | scope: Array.isArray(params.scope) ? params.scope.join(' ') : params.scope, 143 | state: params.state || this.state.create(this.state.secretSync()), 144 | })}`; 145 | 146 | this.log('info', 'The Authorize Uri is :', authUri); 147 | return authUri; 148 | }; 149 | 150 | /** 151 | * Create Token { exchange authorization code for bearer_token } 152 | * * 153 | * @param {string|Object} uri 154 | * @returns {Promise} 155 | */ 156 | OAuthClient.prototype.createToken = function createToken(uri) { 157 | return new Promise((resolve) => { 158 | if (!uri) throw new Error('Provide the Uri'); 159 | const params = queryString.parse(uri.split('?').reverse()[0]); 160 | this.getToken().realmId = params.realmId ? params.realmId : ''; 161 | if ('state' in params) this.getToken().state = params.state; 162 | 163 | const body = {}; 164 | if (params.code) { 165 | body.grant_type = 'authorization_code'; 166 | body.code = params.code; 167 | body.redirect_uri = params.redirectUri || this.redirectUri; 168 | } 169 | 170 | const request = { 171 | url: OAuthClient.tokenEndpoint, 172 | data: body, 173 | method: 'POST', 174 | headers: { 175 | Authorization: `Basic ${this.authHeader()}`, 176 | 'Content-Type': AuthResponse._urlencodedContentType, 177 | Accept: AuthResponse._jsonContentType, 178 | 'User-Agent': OAuthClient.user_agent, 179 | }, 180 | }; 181 | 182 | resolve(this.getTokenRequest(request)); 183 | }) 184 | .then((res) => { 185 | const authResponse = res.hasOwnProperty('json')? res : null; 186 | const json = (authResponse && authResponse.json) || res; 187 | this.token.setToken(json); 188 | this.log('info', 'Create Token response is : ', JSON.stringify(authResponse && authResponse.json, null, 2)); 189 | return authResponse; 190 | }) 191 | .catch((e) => { 192 | this.log('error', 'Create Token () threw an exception : ', JSON.stringify(e, null, 2)); 193 | throw e; 194 | }); 195 | }; 196 | 197 | /** 198 | * Refresh the access_token 199 | * * 200 | * @returns {Promise} 201 | */ 202 | OAuthClient.prototype.refresh = function refresh() { 203 | return new Promise((resolve) => { 204 | this.validateToken(); 205 | 206 | const body = {}; 207 | 208 | body.grant_type = 'refresh_token'; 209 | body.refresh_token = this.getToken().refresh_token; 210 | 211 | const request = { 212 | url: OAuthClient.tokenEndpoint, 213 | data: body, 214 | method: 'POST', 215 | headers: { 216 | Authorization: `Basic ${this.authHeader()}`, 217 | 'Content-Type': AuthResponse._urlencodedContentType, 218 | Accept: AuthResponse._jsonContentType, 219 | 'User-Agent': OAuthClient.user_agent, 220 | }, 221 | }; 222 | 223 | resolve(this.getTokenRequest(request)); 224 | }) 225 | .then((res) => { 226 | const authResponse = res.hasOwnProperty('json')? res : null; 227 | const json = (authResponse && authResponse.json) || res; 228 | this.token.setToken(json); 229 | this.log('info', 'Refresh Token () response is : ', JSON.stringify(authResponse && authResponse.json, null, 2)); 230 | return authResponse; 231 | }) 232 | .catch((e) => { 233 | this.log('error', 'Refresh Token () threw an exception : ', JSON.stringify(e, null, 2)); 234 | throw e; 235 | }); 236 | }; 237 | 238 | /** 239 | * Refresh Tokens by passing refresh_token parameter explicitly 240 | * * 241 | * @param {string} refresh_token 242 | * @returns {Promise} 243 | */ 244 | OAuthClient.prototype.refreshUsingToken = function refreshUsingToken(refresh_token) { 245 | return new Promise((resolve) => { 246 | if (!refresh_token) throw new Error('The Refresh token is missing'); 247 | 248 | const body = {}; 249 | 250 | body.grant_type = 'refresh_token'; 251 | body.refresh_token = refresh_token; 252 | 253 | const request = { 254 | url: OAuthClient.tokenEndpoint, 255 | data: body, 256 | method: 'POST', 257 | headers: { 258 | Authorization: `Basic ${this.authHeader()}`, 259 | 'Content-Type': AuthResponse._urlencodedContentType, 260 | Accept: AuthResponse._jsonContentType, 261 | 'User-Agent': OAuthClient.user_agent, 262 | }, 263 | }; 264 | 265 | resolve(this.getTokenRequest(request)); 266 | }) 267 | .then((res) => { 268 | const authResponse = res.hasOwnProperty('json')? res : null; 269 | const json = (authResponse && authResponse.json) || res; 270 | this.token.setToken(json); 271 | this.log( 272 | 'info', 273 | 'Refresh usingToken () response is : ', JSON.stringify(authResponse && authResponse.json, null, 2), 274 | ); 275 | return authResponse; 276 | }) 277 | .catch((e) => { 278 | this.log('error', 'Refresh Token () threw an exception : ', JSON.stringify(e, null, 2)); 279 | throw e; 280 | }); 281 | }; 282 | 283 | /** 284 | * Revoke access_token/refresh_token 285 | * * 286 | * @param {Object} params.access_token (optional) 287 | * @param {Object} params.refresh_token (optional) 288 | * @returns {Promise} 289 | */ 290 | OAuthClient.prototype.revoke = function revoke(params) { 291 | return new Promise((resolve) => { 292 | params = params || {}; 293 | 294 | const body = {}; 295 | 296 | body.token = 297 | params.access_token || 298 | params.refresh_token || 299 | (this.getToken().isAccessTokenValid() 300 | ? this.getToken().access_token 301 | : this.getToken().refresh_token); 302 | 303 | const request = { 304 | url: OAuthClient.revokeEndpoint, 305 | data: body, 306 | method: 'POST', 307 | headers: { 308 | Authorization: `Basic ${this.authHeader()}`, 309 | Accept: AuthResponse._jsonContentType, 310 | 'Content-Type': AuthResponse._jsonContentType, 311 | 'User-Agent': OAuthClient.user_agent, 312 | }, 313 | }; 314 | 315 | resolve(this.getTokenRequest(request)); 316 | }) 317 | .then((res) => { 318 | const authResponse = res.hasOwnProperty('json')? res : null; 319 | this.token.clearToken(); 320 | this.log('info', 'Revoke Token () response is : ', JSON.stringify(authResponse && authResponse.json, null, 2)); 321 | return authResponse; 322 | }) 323 | .catch((e) => { 324 | this.log('error', 'Revoke Token () threw an exception : ', JSON.stringify(e, null, 2)); 325 | throw e; 326 | }); 327 | }; 328 | 329 | /** 330 | * Get User Info { Get User Info } 331 | * * 332 | * @returns {Promise} 333 | */ 334 | OAuthClient.prototype.getUserInfo = function getUserInfo() { 335 | return new Promise((resolve) => { 336 | const request = { 337 | url: 338 | this.environment === 'sandbox' 339 | ? OAuthClient.userinfo_endpoint_sandbox 340 | : OAuthClient.userinfo_endpoint_production, 341 | method: 'GET', 342 | headers: { 343 | Authorization: `Bearer ${this.token.access_token}`, 344 | Accept: AuthResponse._jsonContentType, 345 | 'User-Agent': OAuthClient.user_agent, 346 | }, 347 | }; 348 | 349 | resolve(this.getTokenRequest(request)); 350 | }) 351 | .then((res) => { 352 | const authResponse = res.hasOwnProperty('json')? res : null; 353 | this.log( 354 | 'info', 355 | 'The Get User Info () response is : ', JSON.stringify(authResponse && authResponse.json, null, 2), 356 | ); 357 | return authResponse; 358 | }) 359 | .catch((e) => { 360 | this.log('error', 'Get User Info () threw an exception : ', JSON.stringify(e, null, 2)); 361 | throw e; 362 | }); 363 | }; 364 | 365 | /** 366 | * Make API call. Pass the url,method,headers using `params` object 367 | * 368 | * @param {params} params 369 | * @param {string} params.url 370 | * @param {string} params.method (optional) default is GET 371 | * @param {Object} params.headers (optional) 372 | * @param {Object} params.body (optional) 373 | * @param {string} params.responseType (optional) default is json - options are json, text, stream, arraybuffer 374 | * @returns {Promise} 375 | */ 376 | OAuthClient.prototype.makeApiCall = function makeApiCall(params) { 377 | return new Promise((resolve) => { 378 | params = params || {}; 379 | const responseType = params.responseType ? params.responseType : 'json'; 380 | 381 | const baseHeaders = { 382 | Authorization: `Bearer ${this.getToken().access_token}`, 383 | Accept: AuthResponse._jsonContentType, 384 | 'User-Agent': OAuthClient.user_agent, 385 | }; 386 | 387 | const headers = 388 | params.headers && typeof params.headers === 'object' 389 | ? Object.assign({}, baseHeaders, params.headers) 390 | : Object.assign({}, baseHeaders); 391 | 392 | const request = { 393 | url: params.url, 394 | method: params.method || 'GET', 395 | headers, 396 | responseType, 397 | }; 398 | 399 | params.body && (request.data = params.body); 400 | 401 | resolve(this.getTokenRequest(request)); 402 | }) 403 | .then((res) => { 404 | const { body, ...authResponse } = res; 405 | this.log('info', 'The makeAPICall () response is : ', JSON.stringify(authResponse.json, null, 2)); 406 | 407 | if(authResponse.json === null && body) { 408 | return { 409 | ...authResponse, 410 | body: body 411 | } 412 | } 413 | return authResponse; 414 | }) 415 | .catch((e) => { 416 | this.log('error', 'Get makeAPICall () threw an exception : ', JSON.stringify(e, null, 2)); 417 | throw e; 418 | }); 419 | }; 420 | 421 | /** 422 | * Validate id_token 423 | * * 424 | * @param {Object} params(optional) 425 | * @returns {Promise} 426 | */ 427 | OAuthClient.prototype.validateIdToken = function validateIdToken(params = {}) { 428 | return new Promise((resolve) => { 429 | if (!this.getToken().id_token) throw new Error('The bearer token does not have id_token'); 430 | 431 | const id_token = this.getToken().id_token || params.id_token; 432 | 433 | // Decode ID Token 434 | const token_parts = id_token.split('.'); 435 | const id_token_header = JSON.parse(atob(token_parts[0])); 436 | const id_token_payload = JSON.parse(atob(token_parts[1])); 437 | 438 | // Step 1 : First check if the issuer is as mentioned in "issuer" 439 | if (id_token_payload.iss !== 'https://oauth.platform.intuit.com/op/v1') return false; 440 | 441 | // Step 2 : check if the aud field in idToken contains application's clientId 442 | if (!id_token_payload.aud.find((audience) => audience === this.clientId)) return false; 443 | 444 | // Step 3 : ensure the timestamp has not elapsed 445 | if (id_token_payload.exp < Date.now() / 1000) return false; 446 | 447 | const request = { 448 | url: OAuthClient.jwks_uri, 449 | method: 'GET', 450 | headers: { 451 | Accept: AuthResponse._jsonContentType, 452 | 'User-Agent': OAuthClient.user_agent, 453 | }, 454 | }; 455 | 456 | return resolve(this.getKeyFromJWKsURI(id_token, id_token_header.kid, request)); 457 | }) 458 | .then((res) => { 459 | this.log('info', 'The validateIdToken () response is :', JSON.stringify(res, null, 2)); 460 | if (res) return true; 461 | return false; 462 | }) 463 | .catch((e) => { 464 | this.log('error', 'The validateIdToken () threw an exception : ', JSON.stringify(e, null, 2)); 465 | throw e; 466 | }); 467 | }; 468 | 469 | /** 470 | * Get Key from JWKURI 471 | * * 472 | * @param {string} id_token 473 | * @param {string} kid 474 | * @param {Object} request 475 | * @returns {Promise} 476 | */ 477 | OAuthClient.prototype.getKeyFromJWKsURI = function getKeyFromJWKsURI(id_token, kid, request) { 478 | return new Promise((resolve) => { 479 | resolve(this.loadResponse(request)); 480 | }) 481 | .then((response) => { 482 | if (Number(response.status) !== 200) throw new Error('Could not reach JWK endpoint'); 483 | // Find the key by KID 484 | const key = response.data.keys.find((el) => el.kid === kid); 485 | const cert = this.getPublicKey(key.n, key.e); 486 | 487 | return jwt.verify(id_token, cert); 488 | }) 489 | .catch((e) => { 490 | e = this.createError(e); 491 | this.log( 492 | 'error', 493 | 'The getKeyFromJWKsURI () threw an exception : ', 494 | JSON.stringify(e, null, 2), 495 | ); 496 | throw e; 497 | }); 498 | }; 499 | 500 | /** 501 | * Get Public Key 502 | * * 503 | * @param modulus 504 | * @param exponent 505 | */ 506 | OAuthClient.prototype.getPublicKey = function getPublicKey(modulus, exponent) { 507 | // eslint-disable-next-line global-require 508 | const getPem = require('rsa-pem-from-mod-exp'); 509 | const pem = getPem(modulus, exponent); 510 | return pem; 511 | }; 512 | 513 | /** 514 | * Get Token Request 515 | * * 516 | * @param {Object} request 517 | * @returns {Promise} 518 | */ 519 | OAuthClient.prototype.getTokenRequest = function getTokenRequest(request) { 520 | const authResponse = new AuthResponse({ 521 | token: this.token, 522 | }); 523 | 524 | return new Promise((resolve) => { 525 | resolve(this.loadResponse(request)); 526 | }) 527 | .then((response) => { 528 | authResponse.processResponse(response); 529 | 530 | if (!authResponse.valid()) throw new Error('Response has an Error'); 531 | 532 | return authResponse; 533 | }) 534 | .catch((e) => { 535 | if (!e.authResponse) e = this.createError(e, authResponse); 536 | throw e; 537 | }); 538 | }; 539 | 540 | /** 541 | * Validate Token 542 | * * 543 | * @returns {boolean} 544 | */ 545 | OAuthClient.prototype.validateToken = function validateToken() { 546 | if (!this.token.refreshToken()) throw new Error('The Refresh token is missing'); 547 | if (!this.token.isRefreshTokenValid()) 548 | throw new Error('The Refresh token is invalid, please Authorize again.'); 549 | }; 550 | 551 | /** 552 | * Make HTTP Request using Axios Client 553 | * @param request 554 | * @returns response 555 | */ 556 | OAuthClient.prototype.loadResponse = function loadResponse(request) { 557 | return axios(request).then((response) => response); 558 | }; 559 | 560 | /** 561 | * Load response from JWK URI 562 | * @param request 563 | * @returns response 564 | */ 565 | OAuthClient.prototype.loadResponseFromJWKsURI = function loadResponseFromJWKsURI(request) { 566 | return axios.get(request).then((response) => response); 567 | }; 568 | 569 | /** 570 | * Wrap the exception with more information 571 | * @param {Error|IApiError} e 572 | * @param {AuthResponse} authResponse 573 | * @return {Error|IApiError} 574 | */ 575 | OAuthClient.prototype.createError = function createError(e, authResponse) { 576 | if (!authResponse || authResponse.body === '') { 577 | e.error = (authResponse && authResponse.response.statusText) || e.message || ''; 578 | e.authResponse = authResponse || ''; 579 | e.intuit_tid = 580 | (authResponse && authResponse.headers() && authResponse.headers().intuit_tid) || ''; 581 | e.originalMessage = e.message || ''; 582 | e.error_description = (authResponse && authResponse.response.statusText) || ''; 583 | return e; 584 | } 585 | 586 | e.authResponse = authResponse; 587 | e.originalMessage = e.message; 588 | 589 | e.error = ''; 590 | if ('error' in authResponse.getJson()) { 591 | e.error = authResponse.getJson().error; 592 | } else if (authResponse.response.statusText) { 593 | e.error = authResponse.response.statusText; 594 | } else if (e.message) { 595 | e.error = e.message; 596 | } 597 | 598 | e.error_description = ''; 599 | if ('error_description' in authResponse.getJson()) { 600 | e.error_description = authResponse.getJson().error_description; 601 | } else if (authResponse.response.statusText) { 602 | e.error_description = authResponse.response.statusText; 603 | } 604 | e.intuit_tid = authResponse.headers().intuit_tid; 605 | 606 | return e; 607 | }; 608 | 609 | /** 610 | * isAccessToken Valid () { TTL of access_token } 611 | * @returns {boolean} 612 | * @private 613 | */ 614 | OAuthClient.prototype.isAccessTokenValid = function isAccessTokenValid() { 615 | return this.token.isAccessTokenValid(); 616 | }; 617 | 618 | /** 619 | * GetToken 620 | * @returns {Token} 621 | */ 622 | OAuthClient.prototype.getToken = function getToken() { 623 | return this.token; 624 | }; 625 | 626 | /** 627 | * Set Token 628 | * @param {Object} 629 | * @returns {Token} 630 | */ 631 | OAuthClient.prototype.setToken = function setToken(params) { 632 | this.token = new Token(params); 633 | return this.token; 634 | }; 635 | 636 | /** 637 | * Get AuthHeader 638 | * @returns {string} authHeader 639 | */ 640 | OAuthClient.prototype.authHeader = function authHeader() { 641 | const apiKey = `${this.clientId}:${this.clientSecret}`; 642 | return typeof btoa === 'function' ? btoa(apiKey) : Buffer.from(apiKey).toString('base64'); 643 | }; 644 | 645 | OAuthClient.prototype.log = function log(level, message, messageData) { 646 | if (this.logging) { 647 | this.logger.log(level, message + messageData); 648 | } 649 | }; 650 | 651 | module.exports = OAuthClient; 652 | -------------------------------------------------------------------------------- /src/access-token/Token.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Copyright (c) 2018 Intuit 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | */ 18 | 19 | /** 20 | * @namespace Token 21 | */ 22 | 23 | 'use strict'; 24 | 25 | /** 26 | * @param {Cache} options.cache 27 | * @param {string} options.cacheId 28 | * @constructor 29 | * @property {Cache} _cache 30 | * @property {string} _cacheId 31 | */ 32 | function Token(params) { 33 | params = params || {}; 34 | 35 | this.realmId = params.realmId || ''; 36 | this.token_type = params.token_type || ''; 37 | this.access_token = params.access_token || ''; 38 | this.refresh_token = params.refresh_token || ''; 39 | this.expires_in = params.expires_in || 0; 40 | this.x_refresh_token_expires_in = params.x_refresh_token_expires_in || 0; 41 | this.id_token = params.id_token || ''; 42 | this.latency = params.latency || 60 * 1000; 43 | this.createdAt = params.createdAt || Date.now(); 44 | } 45 | 46 | /** 47 | * get accessToken() 48 | * @returns {string} access_token 49 | */ 50 | Token.prototype.accessToken = function accessToken() { 51 | return this.getToken().access_token; 52 | }; 53 | 54 | /** 55 | * get refreshToken() 56 | * @returns {string} refresh_token 57 | */ 58 | Token.prototype.refreshToken = function refreshToken() { 59 | return this.getToken().refresh_token; 60 | }; 61 | 62 | /** 63 | * get tokenType() 64 | * @returns {string} token_type 65 | */ 66 | Token.prototype.tokenType = function tokenType() { 67 | return this.getToken().token_type; 68 | }; 69 | 70 | 71 | /** 72 | * Helper Method to get accessToken { get Token Object } 73 | * @returns {{ 74 | * token_type: *, 75 | * access_token: *, 76 | * expires_in: *, 77 | * refresh_token: *, 78 | * x_refresh_token_expires_in: * 79 | * }} 80 | */ 81 | Token.prototype.getToken = function getToken() { 82 | return { 83 | token_type: this.token_type, 84 | access_token: this.access_token, 85 | expires_in: this.expires_in, 86 | refresh_token: this.refresh_token, 87 | x_refresh_token_expires_in: this.x_refresh_token_expires_in, 88 | realmId: this.realmId, 89 | id_token: this.id_token, 90 | createdAt: this.createdAt, 91 | }; 92 | }; 93 | 94 | /** 95 | * Helper Method to set accessToken { set Token Object } 96 | * @param tokenData 97 | * @returns {Token} 98 | */ 99 | Token.prototype.setToken = function setToken(tokenData) { 100 | this.access_token = tokenData.access_token; 101 | this.refresh_token = tokenData.refresh_token; 102 | this.token_type = tokenData.token_type; 103 | this.expires_in = tokenData.expires_in; 104 | this.x_refresh_token_expires_in = tokenData.x_refresh_token_expires_in; 105 | this.id_token = tokenData.id_token || ''; 106 | this.createdAt = tokenData.createdAt || Date.now(); 107 | return this; 108 | }; 109 | 110 | /** 111 | * Helper Method to clear accessToken { clear Token Object } 112 | * @param 113 | * @returns {Token} 114 | */ 115 | Token.prototype.clearToken = function clearToken() { 116 | this.access_token = ''; 117 | this.refresh_token = ''; 118 | this.token_type = ''; 119 | this.expires_in = 0; 120 | this.x_refresh_token_expires_in = 0; 121 | this.id_token = ''; 122 | this.createdAt = 0; 123 | return this; 124 | }; 125 | 126 | /** 127 | * Helper Method to check token expiry { set Token Object } 128 | * @param seconds 129 | * @returns {boolean} 130 | */ 131 | Token.prototype._checkExpiry = function _checkExpiry(seconds) { 132 | const expiry = this.createdAt + (seconds * 1000); 133 | return (expiry - this.latency > Date.now()); 134 | }; 135 | 136 | /** 137 | * Check if access_token is valid 138 | * @returns {boolean} 139 | */ 140 | Token.prototype.isAccessTokenValid = function isAccessTokenValid() { 141 | return this._checkExpiry(this.expires_in); 142 | }; 143 | 144 | /** 145 | * Check if there is a valid (not expired) access token 146 | * @return {boolean} 147 | */ 148 | Token.prototype.isRefreshTokenValid = function isRefreshTokenValid() { 149 | return this._checkExpiry(this.x_refresh_token_expires_in); 150 | }; 151 | 152 | module.exports = Token; 153 | -------------------------------------------------------------------------------- /src/response/AuthResponse.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Copyright (c) 2018 Intuit 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | */ 18 | 19 | /** 20 | * @namespace AuthResponse 21 | */ 22 | 23 | 'use strict'; 24 | 25 | /** 26 | * AuthResponse 27 | * @property {Token} token 28 | * @property {Response} response 29 | * @property {string} body 30 | * @property {object} json 31 | * @property {string} intuit_tid 32 | */ 33 | function AuthResponse(params) { 34 | this.token = params.token || ''; 35 | this.response = params.response || ''; 36 | this.body = params.responseText || ''; 37 | this.json = null; 38 | this.intuit_tid = params.intuit_tid || ''; 39 | } 40 | 41 | /** 42 | * Process Response 43 | * @param response 44 | */ 45 | AuthResponse.prototype.processResponse = function processResponse(response) { 46 | this.response = response || ''; 47 | this.body = (response && response.body) || (response && response.data) || ''; 48 | this.json = this.body && this.isJson() ? this.body : null; 49 | this.intuit_tid = (response && response.headers && response.headers.intuit_tid) || ''; 50 | }; 51 | 52 | /** 53 | * Get Token 54 | * * 55 | * @returns {object} token 56 | */ 57 | AuthResponse.prototype.getToken = function getToken() { 58 | return this.token.getToken(); 59 | }; 60 | 61 | /** 62 | * Get Token 63 | * * 64 | * @returns {string} text 65 | */ 66 | AuthResponse.prototype.text = function text() { 67 | return this.body; 68 | }; 69 | 70 | /** 71 | * Get Token 72 | * * 73 | * @returns {Number} statusCode 74 | */ 75 | AuthResponse.prototype.status = function status() { 76 | return this.response.status; 77 | }; 78 | 79 | /** 80 | * Get response headers 81 | * * 82 | * @returns {Object} headers 83 | */ 84 | AuthResponse.prototype.headers = function headers() { 85 | return this.response.headers; 86 | }; 87 | 88 | /** 89 | * Is Response valid { response is valid ? } 90 | * * 91 | * @returns {*|boolean} 92 | */ 93 | AuthResponse.prototype.valid = function valid() { 94 | return this.response && Number(this.response.status) >= 200 && Number(this.response.status) < 300; 95 | }; 96 | 97 | /** 98 | * Get Json () { returns token as JSON } 99 | * * 100 | * @return {object} json 101 | */ 102 | AuthResponse.prototype.getJson = function getJson() { 103 | if (!this.isJson()) throw new Error('AuthResponse is not JSON'); 104 | if (!this.json) { 105 | this.json = this.body ? JSON.parse(this.body) : null; 106 | } 107 | return this.json; 108 | }; 109 | 110 | /** 111 | * Get Intuit tid 112 | * * 113 | * @returns {string} intuit_tid 114 | */ 115 | AuthResponse.prototype.get_intuit_tid = function get_intuit_tid() { 116 | return this.intuit_tid; 117 | }; 118 | 119 | /** 120 | * isContentType 121 | * * 122 | * @returns {boolean} isContentType 123 | */ 124 | AuthResponse.prototype.isContentType = function isContentType(contentType) { 125 | return this.getContentType().indexOf(contentType) > -1; 126 | }; 127 | 128 | /** 129 | * getContentType 130 | * * 131 | * @returns {string} getContentType 132 | */ 133 | AuthResponse.prototype.getContentType = function getContentType() { 134 | return this.response.headers[AuthResponse._contentType] || ''; 135 | }; 136 | 137 | /** 138 | * isJson 139 | * * 140 | * @returns {boolean} isJson 141 | */ 142 | AuthResponse.prototype.isJson = function isJson() { 143 | return this.isContentType('application/json'); 144 | }; 145 | 146 | AuthResponse._contentType = 'content-type'; 147 | AuthResponse._jsonContentType = 'application/json'; 148 | AuthResponse._urlencodedContentType = 'application/x-www-form-urlencoded'; 149 | 150 | module.exports = AuthResponse; 151 | -------------------------------------------------------------------------------- /test/AuthResponseTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // var nock = require('nock'); 4 | const { 5 | describe, 6 | it, 7 | beforeEach, 8 | afterEach, 9 | } = require('mocha'); 10 | const { expect } = require('chai'); 11 | const sinon = require('sinon'); 12 | 13 | const OAuthClientTest = require('../src/OAuthClient'); 14 | const AuthResponse = require('../src/response/AuthResponse'); 15 | const expectedAccessToken = require('./mocks/bearer-token.json'); 16 | const expectedResponseMock = require('./mocks/response.json'); 17 | const expectedPdfResponseMock = require('./mocks/pdfResponse.json'); 18 | 19 | 20 | const oauthClient = new OAuthClientTest({ 21 | clientId: 'clientID', 22 | clientSecret: 'clientSecret', 23 | environment: 'sandbox', 24 | redirectUri: 'http://localhost:8000/callback', 25 | }); 26 | 27 | oauthClient.getToken().setToken(expectedAccessToken); 28 | 29 | describe('Tests for AuthResponse', () => { 30 | let authResponse; 31 | let getStub; 32 | let expectedResponse; 33 | 34 | beforeEach(() => { 35 | expectedResponse = JSON.parse(JSON.stringify(expectedResponseMock)); 36 | getStub = sinon.stub().returns('application/json;charset=UTF-8'); 37 | expectedResponse.get = getStub; 38 | 39 | authResponse = new AuthResponse({ token: oauthClient.getToken() }); 40 | authResponse.processResponse(expectedResponse); 41 | }); 42 | 43 | afterEach(() => { 44 | getStub.reset(); 45 | }); 46 | 47 | it('Creates a new auth response instance', () => { 48 | expect(authResponse).to.have.property('token'); 49 | expect(authResponse).to.have.property('response'); 50 | expect(authResponse).to.have.property('body'); 51 | expect(authResponse).to.have.property('json'); 52 | expect(authResponse).to.have.property('intuit_tid'); 53 | }); 54 | 55 | it('Process Response', () => { 56 | authResponse.processResponse(expectedResponse); 57 | expect(authResponse.response).to.deep.equal(expectedResponse); 58 | expect(authResponse.intuit_tid).to.deep.equal(expectedResponse.headers.intuit_tid); 59 | }); 60 | 61 | it('Process Get Token', () => { 62 | const token = authResponse.getToken(); 63 | expect(token).to.have.property('token_type'); 64 | expect(token).to.have.property('refresh_token'); 65 | expect(token).to.have.property('expires_in'); 66 | expect(token).to.have.property('x_refresh_token_expires_in'); 67 | }); 68 | 69 | it('Process Text() when there is body ', () => { 70 | const text = authResponse.body; 71 | expect(text).to.be.a('string'); 72 | expect(text).to.be.equal('{"id_token":"sample_id_token","expires_in":3600,"token_type":"bearer","x_refresh_token_expires_in":8726400,"refresh_token":"sample_refresh_token","access_token":"sample_access_token"}'); 73 | }); 74 | 75 | it('Process Status of AuthResponse', () => { 76 | const status = authResponse.status(); 77 | expect(status).to.be.equal(200); 78 | }); 79 | 80 | it('Process Headers of AuthResponse', () => { 81 | const headers = authResponse.headers(); 82 | expect(headers).to.be.equal(expectedResponse.headers); 83 | }); 84 | 85 | it('Process Get Json', () => { 86 | const json = authResponse.body; 87 | expect(json).to.be.equal(expectedResponse.body); 88 | }); 89 | 90 | xit('Process Get Json when content type is not correct to throw an error', () => { 91 | getStub.returns('blah'); 92 | authResponse.processResponse(expectedResponse); 93 | expect(() => authResponse.getJson()).to.throw(Error); 94 | }); 95 | 96 | it('Process Get Json empty Body', () => { 97 | delete expectedResponse.body; 98 | authResponse = new AuthResponse({}); 99 | authResponse.processResponse(expectedResponse); 100 | expect(authResponse.getJson()).to.be.equal(null); 101 | 102 | // Test putting the body back for branch coverage 103 | authResponse.body = expectedResponseMock.body; 104 | const json = authResponse.getJson(); 105 | expect(JSON.stringify(json)).to.be.equal(JSON.stringify(JSON.parse(expectedResponseMock.body))); 106 | }); 107 | 108 | it('GetContentType should handle False', () => { 109 | getStub.returns(false); 110 | expectedResponse.headers = getStub; 111 | // delete expectedResponse.contentType; 112 | authResponse = new AuthResponse({}); 113 | authResponse.processResponse(expectedResponse); 114 | expect(authResponse.getContentType()).to.be.equal(''); 115 | }); 116 | 117 | it('Process get_intuit_tid', () => { 118 | const intuitTid = authResponse.get_intuit_tid(); 119 | expect(intuitTid).to.be.equal(expectedResponseMock.headers.intuit_tid); 120 | }); 121 | 122 | it('ProcessResponse should handle empty response', () => { 123 | expect(() => authResponse.processResponse(null)).to.not.throw(); 124 | }); 125 | }); 126 | 127 | describe('Tests for AuthResponse with not json content', () => { 128 | let authResponse; 129 | let getStub; 130 | let expectedResponse; 131 | 132 | beforeEach(() => { 133 | expectedResponse = JSON.parse(JSON.stringify(expectedPdfResponseMock)); 134 | getStub = sinon.stub().returns('application/pdf'); 135 | expectedResponse.get = getStub; 136 | 137 | authResponse = new AuthResponse({ token: oauthClient.getToken() }); 138 | authResponse.processResponse(expectedResponse); 139 | }); 140 | 141 | afterEach(() => { 142 | getStub.reset(); 143 | }); 144 | 145 | it('Creates a new auth response instance', () => { 146 | expect(authResponse).to.have.property('token'); 147 | expect(authResponse).to.have.property('response'); 148 | expect(authResponse).to.have.property('body'); 149 | expect(authResponse).to.have.property('json'); 150 | expect(authResponse).to.have.property('intuit_tid'); 151 | }); 152 | 153 | it('Process Response', () => { 154 | authResponse.processResponse(expectedResponse); 155 | expect(authResponse.response).to.deep.equal(expectedResponse); 156 | }); 157 | 158 | it('Process Get Token', () => { 159 | const token = authResponse.getToken(); 160 | expect(token).to.have.property('token_type'); 161 | expect(token).to.have.property('refresh_token'); 162 | expect(token).to.have.property('expires_in'); 163 | expect(token).to.have.property('x_refresh_token_expires_in'); 164 | }); 165 | 166 | it('Process Text() when there is body ', () => { 167 | const text = authResponse.text(); 168 | expect(text).to.be.a('string'); 169 | expect(text).to.be.equal('%PDF-1.\ntrailer<>]>>>>>>'); 170 | }); 171 | 172 | it('Process Status of AuthResponse', () => { 173 | const status = authResponse.status(); 174 | expect(status).to.be.equal(200); 175 | }); 176 | 177 | it('Process Headers of AuthResponse', () => { 178 | const headers = authResponse.headers(); 179 | expect(headers).to.be.equal(expectedResponse.headers); 180 | }); 181 | 182 | it('Process Get Json to throw an error', () => { 183 | expect(() => authResponse.getJson()).to.throw(Error); 184 | }); 185 | 186 | it('GetContentType should handle False', () => { 187 | const contentType = authResponse.getContentType(); 188 | expect(contentType).to.be.equal('application/pdf'); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /test/OAuthClientTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { describe, it, before, beforeEach, afterEach } = require('mocha'); 4 | const nock = require('nock'); 5 | const sinon = require('sinon'); 6 | const chai = require('chai'); 7 | const chaiAsPromised = require('chai-as-promised'); 8 | const btoa = require('btoa'); 9 | const jwt = require('jsonwebtoken'); 10 | 11 | // eslint-disable-next-line no-unused-vars 12 | const getPem = require('rsa-pem-from-mod-exp'); 13 | 14 | const AuthResponse = require('../src/response/AuthResponse'); 15 | const OAuthClientTest = require('../src/OAuthClient'); 16 | // var AuthResponse = require('../src/response/AuthResponse'); 17 | const expectedAccessToken = require('./mocks/bearer-token.json'); 18 | const expectedTokenResponse = require('./mocks/tokenResponse.json'); 19 | const expectedUserInfo = require('./mocks/userInfo.json'); 20 | const expectedMakeAPICall = require('./mocks/makeAPICallResponse.json'); 21 | const expectedjwkResponseCall = require('./mocks/jwkResponse.json'); 22 | const expectedOpenIDToken = require('./mocks/openID-token.json'); 23 | // var expectedErrorResponse = require('./mocks/errorResponse.json'); 24 | const expectedMigrationResponse = require('./mocks/authResponse.json'); 25 | 26 | require.cache[require.resolve('rsa-pem-from-mod-exp')] = { 27 | exports: sinon.stub().returns(3), 28 | }; 29 | 30 | const oauthClient = new OAuthClientTest({ 31 | clientId: 'clientID', 32 | clientSecret: 'clientSecret', 33 | environment: 'sandbox', 34 | redirectUri: 'http://localhost:8000/callback', 35 | logging: true, 36 | }); 37 | 38 | const { expect } = chai; 39 | chai.use(chaiAsPromised); 40 | 41 | describe('Tests for OAuthClient', () => { 42 | it('Creates a new access token instance', () => { 43 | const accessToken = oauthClient.getToken(); 44 | expect(accessToken).to.have.property('realmId'); 45 | expect(accessToken).to.have.property('token_type'); 46 | expect(accessToken).to.have.property('refresh_token'); 47 | expect(accessToken).to.have.property('expires_in'); 48 | expect(accessToken).to.have.property('x_refresh_token_expires_in'); 49 | expect(accessToken).to.have.property('id_token'); 50 | expect(accessToken).to.have.property('latency'); 51 | }); 52 | 53 | describe('Get the authorizationURI', () => { 54 | it('When Scope is passed', () => { 55 | const actualAuthUri = oauthClient.authorizeUri({ scope: 'testScope', state: 'testState' }); 56 | const expectedAuthUri = 57 | 'https://appcenter.intuit.com/connect/oauth2?client_id=clientID&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fcallback&response_type=code&scope=testScope&state=testState'; 58 | expect(actualAuthUri).to.be.equal(expectedAuthUri); 59 | }); 60 | 61 | it('When NO Scope is passed', () => { 62 | try { 63 | oauthClient.authorizeUri(); 64 | } catch (e) { 65 | expect(e.message).to.equal('Provide the scopes'); 66 | } 67 | }); 68 | it('When Scope is passed as an array', () => { 69 | const actualAuthUri = oauthClient.authorizeUri({ 70 | scope: [ 71 | OAuthClientTest.scopes.Accounting, 72 | OAuthClientTest.scopes.Payment, 73 | OAuthClientTest.scopes.OpenId, 74 | ], 75 | state: 'testState', 76 | }); 77 | const expectedAuthUri = 78 | 'https://appcenter.intuit.com/connect/oauth2?client_id=clientID&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fcallback&response_type=code&scope=com.intuit.quickbooks.accounting%20com.intuit.quickbooks.payment%20openid&state=testState'; 79 | expect(actualAuthUri).to.be.equal(expectedAuthUri); 80 | }); 81 | }); 82 | 83 | // Create bearer tokens 84 | describe('Create Bearer Token', () => { 85 | before(() => { 86 | nock('https://oauth.platform.intuit.com') 87 | .persist() 88 | .post('/oauth2/v1/tokens/bearer') 89 | .reply(200, expectedTokenResponse, { 90 | 'content-type': 'application/json', 91 | 'content-length': '1636', 92 | connection: 'close', 93 | server: 'nginx', 94 | intuit_tid: '12345-123-1234-12345', 95 | 'cache-control': 'no-cache, no-store', 96 | pragma: 'no-cache', 97 | }); 98 | }); 99 | 100 | it('Provide the uri to get the tokens', () => { 101 | const parseRedirect = 102 | 'http://localhost:8000/callback?state=testState&code=Q011535008931rqveFweqmueq0GlOHhLPAFMp3NI2KJm5gbMMx'; 103 | return oauthClient.createToken(parseRedirect).then((authResponse) => { 104 | expect(authResponse.getToken().access_token).to.be.equal(expectedAccessToken.access_token); 105 | }); 106 | }); 107 | 108 | it('When NO uri is provided', () => 109 | oauthClient 110 | .createToken() 111 | .then((authResponse) => { 112 | expect(authResponse.getToken().access_token).to.be.equal( 113 | expectedAccessToken.access_token, 114 | ); 115 | }) 116 | .catch((e) => { 117 | expect(e.message).to.equal('Provide the Uri'); 118 | })); 119 | 120 | it('Handles when code is NOT in the URL', async () => { 121 | const parseRedirect = 'http://localhost:8000/callback?state=testState'; 122 | const authResponse = await oauthClient.createToken(parseRedirect); 123 | expect(authResponse.getToken().access_token).to.be.equal(expectedAccessToken.access_token); 124 | }); 125 | 126 | it('handles a realm id in the url', async () => { 127 | const parseRedirect = 'http://localhost:8000/callback?state=testState&realmId=12345'; 128 | const authResponse = await oauthClient.createToken(parseRedirect); 129 | expect(authResponse.getToken().access_token).to.be.equal(expectedAccessToken.access_token); 130 | }); 131 | }); 132 | 133 | // Refresh bearer tokens 134 | describe('Refresh Bearer Token', () => { 135 | before(() => { 136 | // eslint-disable-next-line global-require 137 | const refreshAccessToken = require('./mocks/refreshResponse.json'); 138 | nock('https://oauth.platform.intuit.com') 139 | .persist() 140 | .post('/oauth2/v1/tokens/bearer') 141 | .reply(200, refreshAccessToken, { 142 | 'content-type': 'application/json', 143 | 'content-length': '1636', 144 | connection: 'close', 145 | server: 'nginx', 146 | intuit_tid: '12345-123-1234-12345', 147 | 'cache-control': 'no-cache, no-store', 148 | pragma: 'no-cache', 149 | }); 150 | }); 151 | 152 | it('Refresh the existing tokens', () => 153 | oauthClient.refresh().then((authResponse) => { 154 | expect(authResponse.getToken().refresh_token).to.be.equal( 155 | expectedAccessToken.refresh_token, 156 | ); 157 | })); 158 | 159 | it('Refresh : refresh token is missing', () => { 160 | oauthClient.getToken().refresh_token = null; 161 | return oauthClient.refresh().catch((e) => { 162 | expect(e.message).to.equal('The Refresh token is missing'); 163 | }); 164 | }); 165 | 166 | it('Refresh : refresh token is invalid', () => { 167 | oauthClient.getToken().refresh_token = 'sample_refresh_token'; 168 | oauthClient.getToken().x_refresh_token_expires_in = '300'; 169 | return oauthClient.refresh().catch((e) => { 170 | expect(e.message).to.equal('The Refresh token is invalid, please Authorize again.'); 171 | }); 172 | }); 173 | 174 | it('Refresh Using token', async () => { 175 | const refreshToken = expectedAccessToken.refresh_token; 176 | await oauthClient.refreshUsingToken(refreshToken); 177 | expect(oauthClient.getToken().refresh_token).to.be.equal(refreshToken); 178 | }); 179 | 180 | it('Handle refresh using token with empty token', async () => { 181 | await expect(oauthClient.refreshUsingToken(null)).to.be.rejectedWith(Error); 182 | }); 183 | }); 184 | 185 | // Revoke bearer tokens 186 | describe('Revoke Bearer Token', () => { 187 | before(() => { 188 | nock('https://developer.api.intuit.com') 189 | .persist() 190 | .post('/v2/oauth2/tokens/revoke') 191 | .reply(200, '', { 192 | 'content-type': 'application/json', 193 | 'content-length': '1636', 194 | connection: 'close', 195 | server: 'nginx', 196 | intuit_tid: '12345-123-1234-12345', 197 | 'cache-control': 'no-cache, no-store', 198 | pragma: 'no-cache', 199 | }); 200 | }); 201 | 202 | it('Revoke the existing tokens', () => { 203 | oauthClient.getToken().x_refresh_token_expires_in = '4535995551112'; 204 | return oauthClient.revoke().then((authResponse) => { 205 | expect(authResponse.getToken().refresh_token).to.be.equal(''); 206 | }); 207 | }); 208 | 209 | it('Revoke : refresh token is missing', () => { 210 | oauthClient.getToken().refresh_token = null; 211 | return oauthClient.revoke().catch((e) => { 212 | expect(e.message).to.equal('The Refresh token is missing'); 213 | }); 214 | }); 215 | 216 | it('Revoke : refresh token is invalid', () => { 217 | oauthClient.getToken().refresh_token = 'sample_refresh_token'; 218 | oauthClient.getToken().x_refresh_token_expires_in = '300'; 219 | return oauthClient.revoke().catch((e) => { 220 | expect(e.message).to.equal('The Refresh token is invalid, please Authorize again.'); 221 | }); 222 | }); 223 | }); 224 | 225 | // Get User Info ( OpenID ) 226 | describe('Get User Info ( OpenID )', () => { 227 | before(() => { 228 | nock('https://sandbox-accounts.platform.intuit.com') 229 | .persist() 230 | .get('/v1/openid_connect/userinfo') 231 | .reply(200, expectedUserInfo, { 232 | 'content-type': 'application/json', 233 | 'content-length': '1636', 234 | connection: 'close', 235 | server: 'nginx', 236 | intuit_tid: '12345-123-1234-12345', 237 | 'cache-control': 'no-cache, no-store', 238 | pragma: 'no-cache', 239 | }); 240 | }); 241 | 242 | it('Get User Info in Sandbox', () => 243 | oauthClient.getUserInfo().then((authResponse) => { 244 | expect(JSON.stringify(authResponse.json)).to.be.equal( 245 | JSON.stringify(expectedUserInfo), 246 | ); 247 | })); 248 | }); 249 | 250 | describe('Get User Info In Production', () => { 251 | before(() => { 252 | nock('https://accounts.platform.intuit.com') 253 | .persist() 254 | .get('/v1/openid_connect/userinfo') 255 | .reply(200, expectedUserInfo, { 256 | 'content-type': 'application/json', 257 | 'content-length': '1636', 258 | connection: 'close', 259 | server: 'nginx', 260 | intuit_tid: '12345-123-1234-12345', 261 | 'cache-control': 'no-cache, no-store', 262 | pragma: 'no-cache', 263 | }); 264 | }); 265 | 266 | it('Get User Info in Production', () => { 267 | oauthClient.environment = 'production'; 268 | return oauthClient.getUserInfo().then((authResponse) => { 269 | expect(JSON.stringify(authResponse.json)).to.be.equal( 270 | JSON.stringify(expectedUserInfo), 271 | ); 272 | }); 273 | }); 274 | }); 275 | 276 | // make API Call 277 | describe('Make API Call', () => { 278 | before(() => { 279 | nock('https://sandbox-quickbooks.api.intuit.com') 280 | .persist() 281 | .get('/v3/company/12345/companyinfo/12345') 282 | .reply(200, expectedMakeAPICall, { 283 | 'content-type': 'application/json', 284 | 'content-length': '1636', 285 | connection: 'close', 286 | server: 'nginx', 287 | intuit_tid: '12345-123-1234-12345', 288 | 'cache-control': 'no-cache, no-store', 289 | pragma: 'no-cache', 290 | }); 291 | }); 292 | it('Make API Call in Sandbox Environment', () => { 293 | oauthClient.getToken().realmId = '12345'; 294 | // eslint-disable-next-line no-useless-concat 295 | return oauthClient 296 | .makeApiCall({ 297 | url: 298 | 'https://sandbox-quickbooks.api.intuit.com/v3/company/' + 299 | '12345' + 300 | '/companyinfo/' + 301 | '12345', 302 | }) 303 | .then((authResponse) => { 304 | expect(JSON.stringify(authResponse.json)).to.be.equal( 305 | JSON.stringify(expectedMakeAPICall), 306 | ); 307 | }); 308 | }); 309 | it('Make API Call in Sandbox Environment with headers as parameters', () => { 310 | oauthClient.getToken().realmId = '12345'; 311 | // eslint-disable-next-line no-useless-concat 312 | return oauthClient 313 | .makeApiCall({ 314 | url: `https://sandbox-quickbooks.api.intuit.com/v3/company/12345/companyinfo/12345`, 315 | headers: { 316 | Accept: 'application/json', 317 | }, 318 | }) 319 | .then((authResponse) => { 320 | expect(JSON.stringify(authResponse.json)).to.be.equal( 321 | JSON.stringify(expectedMakeAPICall), 322 | ); 323 | }); 324 | }); 325 | xit('loadResponseFromJWKsURI', () => { 326 | const request = { 327 | url: 'https://sandbox-quickbooks.api.intuit.com/v3/company/12345/companyinfo/12345', 328 | }; 329 | return oauthClient.loadResponseFromJWKsURI(request).then((authResponse) => { 330 | expect(authResponse.body).to.be.equal(JSON.stringify(expectedMakeAPICall)); 331 | }); 332 | }); 333 | }); 334 | 335 | describe('Make API call in Production', () => { 336 | before(() => { 337 | nock('https://quickbooks.api.intuit.com') 338 | .persist() 339 | .get('/v3/company/12345/companyinfo/12345') 340 | .reply(200, expectedMakeAPICall, { 341 | 'content-type': 'application/json', 342 | 'content-length': '1636', 343 | connection: 'close', 344 | server: 'nginx', 345 | intuit_tid: '12345-123-1234-12345', 346 | 'cache-control': 'no-cache, no-store', 347 | pragma: 'no-cache', 348 | }); 349 | }); 350 | it('Make API Call in Production Environment', () => { 351 | oauthClient.environment = 'production'; 352 | oauthClient.getToken().realmId = '12345'; 353 | // eslint-disable-next-line no-useless-concat 354 | return oauthClient 355 | .makeApiCall({ 356 | url: 357 | 'https://quickbooks.api.intuit.com/v3/company/' + '12345' + '/companyinfo/' + '12345', 358 | }) 359 | .then((authResponse) => { 360 | expect(JSON.stringify(authResponse.json)).to.be.equal( 361 | JSON.stringify(expectedMakeAPICall), 362 | ); 363 | }); 364 | }); 365 | }); 366 | }); 367 | 368 | describe('getPublicKey', () => { 369 | const pem = oauthClient.getPublicKey(3, 4); 370 | expect(pem).to.be.equal(3); 371 | }); 372 | 373 | describe('Validate that token request can handle a failure', () => { 374 | before(() => { 375 | nock('https://sandbox-quickbooks.api.intuit.com') 376 | .persist() 377 | .get('/v3/company/6789/companyinfo/6789') 378 | .reply(416, expectedMakeAPICall, { 379 | 'content-type': 'application/json', 380 | 'content-length': '1636', 381 | connection: 'close', 382 | server: 'nginx', 383 | intuit_tid: '12345-123-1234-12345', 384 | 'cache-control': 'no-cache, no-store', 385 | pragma: 'no-cache', 386 | }); 387 | }); 388 | 389 | it('Validate token request can handle a failure', async () => { 390 | oauthClient.getToken().setToken(expectedOpenIDToken); 391 | await expect( 392 | oauthClient.getTokenRequest({ 393 | url: 'https://sandbox-quickbooks.api.intuit.com/v3/company/6789/companyinfo/6789', 394 | }), 395 | ).to.be.rejectedWith(Error); 396 | }); 397 | }); 398 | 399 | // Validate Id Token 400 | describe('Validate Id Token ', () => { 401 | before(() => { 402 | nock('https://oauth.platform.intuit.com') 403 | .persist() 404 | .get('/op/v1/jwks') 405 | .reply(200, expectedjwkResponseCall.body, { 406 | 'content-type': 'application/json;charset=UTF-8', 407 | 'content-length': '264', 408 | connection: 'close', 409 | server: 'nginx', 410 | 'strict-transport-security': 'max-age=15552000', 411 | intuit_tid: '1234-1234-1234-123', 412 | 'cache-control': 'no-cache, no-store', 413 | pragma: 'no-cache', 414 | }); 415 | sinon.stub(jwt, 'verify').returns(true); 416 | }); 417 | 418 | const mockIdTokenPayload = { 419 | sub: 'b053d994-07d5-468d-b7ee-22e349d2e739', 420 | aud: ['clientID'], 421 | realmid: '1108033471', 422 | auth_time: 1462554475, 423 | iss: 'https://oauth.platform.intuit.com/op/v1', 424 | exp: Date.now() + 60000, 425 | iat: 1462557728, 426 | }; 427 | 428 | const tokenParts = expectedOpenIDToken.id_token.split('.'); 429 | const encodedMockIdTokenPayload = tokenParts[0].concat( 430 | '.', 431 | btoa(JSON.stringify(mockIdTokenPayload)), 432 | ); 433 | const mockToken = Object.assign({}, expectedOpenIDToken, { id_token: encodedMockIdTokenPayload }); 434 | 435 | it('validate id token returns error if id_token missing', async () => { 436 | delete oauthClient.getToken().id_token; 437 | await expect(oauthClient.validateIdToken()).to.be.rejectedWith(Error); 438 | }); 439 | 440 | it('Validate Id Token', () => { 441 | oauthClient.getToken().setToken(mockToken); 442 | oauthClient.validateIdToken().then((response) => { 443 | expect(response).to.be.equal(true); 444 | }); 445 | }); 446 | 447 | it('Validate Id Token alternative', () => { 448 | oauthClient.setToken(mockToken); 449 | oauthClient.validateIdToken().then((response) => { 450 | expect(response).to.be.equal(true); 451 | }); 452 | }); 453 | }); 454 | 455 | // Validate Refresh Token 456 | describe('Validate Refresh Token', () => { 457 | it('Validate should handle expired token', () => { 458 | const newToken = JSON.parse(JSON.stringify(expectedOpenIDToken)); 459 | newToken.createdAt = new Date(1970, 1, 1); 460 | oauthClient.setToken(newToken); 461 | expect(() => oauthClient.validateToken()).to.throw(Error); 462 | }); 463 | }); 464 | 465 | // Check Access Token Validity 466 | describe('Check Access-Token Validity', () => { 467 | before(() => { 468 | // Reset token 469 | oauthClient.setToken(expectedAccessToken); 470 | }); 471 | it('access-token is valid', () => { 472 | const validity = oauthClient.isAccessTokenValid(); 473 | // eslint-disable-next-line no-unused-expressions 474 | expect(validity).to.be.true; 475 | }); 476 | it('access-token is not valid', () => { 477 | oauthClient.getToken().expires_in = null; 478 | const validity = oauthClient.isAccessTokenValid(); 479 | // eslint-disable-next-line no-unused-expressions 480 | expect(validity).to.be.false; 481 | }); 482 | }); 483 | 484 | // Get Token 485 | describe('Get Token', () => { 486 | it('get token instance', () => { 487 | const token = oauthClient.getToken(); 488 | expect(token).to.be.a('Object'); 489 | }); 490 | it('accesstoken is not valid', () => { 491 | oauthClient.getToken().expires_in = null; 492 | const validity = oauthClient.isAccessTokenValid(); 493 | // eslint-disable-next-line no-unused-expressions 494 | expect(validity).to.be.false; 495 | }); 496 | }); 497 | 498 | // Get Auth Header 499 | describe('Get Auth Header', () => { 500 | it('Auth Header is valid', () => { 501 | let authHeader = oauthClient.authHeader(); 502 | expect(authHeader).to.be.equal('Y2xpZW50SUQ6Y2xpZW50U2VjcmV0'); 503 | 504 | global.btoa = sinon.stub().returns('abc'); 505 | authHeader = oauthClient.authHeader(); 506 | expect(authHeader).to.be.equal('abc'); 507 | delete global.btoa; 508 | }); 509 | it('accesstoken is not valid', () => { 510 | oauthClient.getToken().expires_in = null; 511 | const validity = oauthClient.isAccessTokenValid(); 512 | // eslint-disable-next-line no-unused-expressions 513 | expect(validity).to.be.false; 514 | }); 515 | }); 516 | 517 | // Load Responses 518 | describe('load responses', () => { 519 | before(() => { 520 | nock('https://sandbox-quickbooks.api.intuit.com') 521 | .persist() 522 | .get('/v3/company/12345/companyinfo/12345') 523 | .reply(200, expectedMakeAPICall, { 524 | 'content-type': 'application/json', 525 | 'content-length': '1636', 526 | connection: 'close', 527 | server: 'nginx', 528 | intuit_tid: '12345-123-1234-12345', 529 | 'cache-control': 'no-cache, no-store', 530 | pragma: 'no-cache', 531 | }); 532 | }); 533 | }); 534 | 535 | describe('Test Create Error Wrapper', () => { 536 | let authResponse; 537 | let expectedAuthResponse; 538 | let getStub; 539 | 540 | beforeEach(() => { 541 | expectedAuthResponse = JSON.parse(JSON.stringify(expectedMigrationResponse.response)); 542 | getStub = sinon.stub().returns('application/json;charset=UTF-8'); 543 | expectedAuthResponse.get = getStub; 544 | authResponse = new AuthResponse({ token: oauthClient.getToken() }); 545 | authResponse.processResponse(expectedAuthResponse); 546 | }); 547 | 548 | afterEach(() => { 549 | getStub.reset(); 550 | }); 551 | 552 | it('Should handle an empty error and empty authResponse', () => { 553 | const wrappedE = oauthClient.createError(new Error(), null); 554 | expect(wrappedE.error).to.be.equal(''); 555 | expect(wrappedE.authResponse).to.be.equal(''); 556 | expect(wrappedE.intuit_tid).to.be.equal(''); 557 | expect(wrappedE.originalMessage).to.be.equal(''); 558 | expect(wrappedE.error_description).to.be.equal(''); 559 | }); 560 | 561 | it('Should handle an error with text and empty authResponse', () => { 562 | const errorMessage = 'error foo'; 563 | const wrappedE = oauthClient.createError(new Error(errorMessage), null); 564 | expect(wrappedE.error).to.be.equal(errorMessage); 565 | expect(wrappedE.authResponse).to.be.equal(''); 566 | expect(wrappedE.intuit_tid).to.be.equal(''); 567 | expect(wrappedE.originalMessage).to.be.equal(errorMessage); 568 | expect(wrappedE.error_description).to.be.equal(''); 569 | }); 570 | 571 | it('should handle an authResponse with no body', () => { 572 | authResponse.body = ''; 573 | const wrappedE = oauthClient.createError(new Error(), authResponse); 574 | expect(wrappedE.error).to.be.equal(authResponse.response.statusText); 575 | expect(JSON.stringify(wrappedE.authResponse)).to.be.equal(JSON.stringify(authResponse)); 576 | expect(wrappedE.intuit_tid).to.be.equal(authResponse.response.headers.intuit_tid); 577 | expect(wrappedE.originalMessage).to.be.equal(''); 578 | expect(wrappedE.error_description).to.be.equal(authResponse.response.statusText); 579 | }); 580 | 581 | it('should handle an authResponse', () => { 582 | const errorMessage = 'error foo'; 583 | authResponse.body = ''; 584 | const wrappedE = oauthClient.createError(new Error(errorMessage), authResponse); 585 | expect(wrappedE.error).to.be.equal(authResponse.response.statusText); 586 | expect(JSON.stringify(wrappedE.authResponse)).to.be.equal(JSON.stringify(authResponse)); 587 | expect(wrappedE.intuit_tid).to.be.equal(authResponse.response.headers.intuit_tid); 588 | expect(wrappedE.originalMessage).to.be.equal(errorMessage); 589 | expect(wrappedE.error_description).to.be.equal(authResponse.response.statusText); 590 | }); 591 | 592 | it('should handle an authResponse with a body that contains error info', () => { 593 | const originalErrorMessage = 'error foobar'; 594 | const errorMessage = 'error foo'; 595 | const errorDescription = 'error bar'; 596 | const errorJson = { 597 | error: errorMessage, 598 | error_description: errorDescription, 599 | }; 600 | authResponse.json = errorJson; 601 | authResponse.body = errorJson; 602 | 603 | let wrappedE = oauthClient.createError(new Error(originalErrorMessage), authResponse); 604 | expect(wrappedE.error).to.be.equal(errorMessage); 605 | expect(JSON.stringify(wrappedE.authResponse)).to.be.equal(JSON.stringify(authResponse)); 606 | expect(wrappedE.intuit_tid).to.be.equal(authResponse.response.headers.intuit_tid); 607 | expect(wrappedE.originalMessage).to.be.equal(originalErrorMessage); 608 | expect(wrappedE.error_description).to.be.equal(errorDescription); 609 | 610 | delete errorJson.error; 611 | authResponse.json = errorJson; 612 | authResponse.body = errorJson; 613 | delete authResponse.response.statusText; 614 | wrappedE = oauthClient.createError(new Error(originalErrorMessage), authResponse); 615 | expect(wrappedE.error).to.be.equal(originalErrorMessage); 616 | 617 | wrappedE = oauthClient.createError(new Error(), authResponse); 618 | expect(wrappedE.error).to.be.equal(''); 619 | }); 620 | }); 621 | 622 | describe('Test Logging', () => { 623 | it('Should handle a log', () => { 624 | oauthClient.logger = { 625 | log: sinon.spy(), 626 | }; 627 | oauthClient.logging = true; 628 | const level = 'DEBUG'; 629 | const message = 'Message'; 630 | const messageData = 'Data'; 631 | 632 | oauthClient.log(level, message, messageData); 633 | 634 | expect(oauthClient.logger.log.firstCall.args[0]).to.be.equal(level); 635 | expect(oauthClient.logger.log.firstCall.args[1]).to.be.equal(message + messageData); 636 | }); 637 | }); 638 | -------------------------------------------------------------------------------- /test/TokenTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | 'use strict'; 4 | 5 | const { describe, it } = require('mocha'); 6 | const { expect } = require('chai'); 7 | 8 | const OAuthClientTest = require('../src/OAuthClient'); 9 | const expectedAccessToken = require('./mocks/bearer-token.json'); 10 | 11 | 12 | let oauthClient = new OAuthClientTest({ 13 | clientId: 'clientID', 14 | clientSecret: 'clientSecret', 15 | environment: 'sandbox', 16 | redirectUri: 'http://localhost:8000/callback', 17 | }); 18 | 19 | describe('Tests for Token', () => { 20 | it('Creates a new token instance', () => { 21 | const token = oauthClient.getToken(); 22 | expect(token).to.have.property('realmId'); 23 | expect(token).to.have.property('token_type'); 24 | expect(token).to.have.property('access_token'); 25 | expect(token).to.have.property('refresh_token'); 26 | expect(token).to.have.property('expires_in'); 27 | expect(token).to.have.property('x_refresh_token_expires_in'); 28 | expect(token).to.have.property('latency'); 29 | expect(token).to.have.property('id_token'); 30 | }); 31 | 32 | it('Set Token using Constructor', () => { 33 | oauthClient = new OAuthClientTest({ 34 | clientId: 'clientID', 35 | clientSecret: 'clientSecret', 36 | environment: 'sandbox', 37 | redirectUri: 'http://localhost:8000/callback', 38 | token: expectedAccessToken, 39 | }); 40 | const token = oauthClient.getToken(); 41 | 42 | expect(token.access_token).to.equal(expectedAccessToken.access_token); 43 | expect(token.refresh_token).to.equal(expectedAccessToken.refresh_token); 44 | expect(token.token_type).to.equal(expectedAccessToken.token_type); 45 | expect(token.expires_in).to.equal(expectedAccessToken.expires_in); 46 | expect(token.x_refresh_token_expires_in) 47 | .to.equal(expectedAccessToken.x_refresh_token_expires_in); 48 | }); 49 | 50 | it('Set Token using Helper Method', () => { 51 | oauthClient.token.setToken(expectedAccessToken); 52 | const token = oauthClient.getToken(); 53 | 54 | expect(token.access_token).to.equal(expectedAccessToken.access_token); 55 | expect(token.refresh_token).to.equal(expectedAccessToken.refresh_token); 56 | expect(token.token_type).to.equal(expectedAccessToken.token_type); 57 | expect(token.expires_in).to.equal(expectedAccessToken.expires_in); 58 | expect(token.x_refresh_token_expires_in) 59 | .to.equal(expectedAccessToken.x_refresh_token_expires_in); 60 | }); 61 | 62 | it('Get Access Token using Helper Method', () => { 63 | oauthClient.token.setToken(expectedAccessToken); 64 | const accessToken = oauthClient.getToken().accessToken(); 65 | 66 | expect(accessToken).to.deep.equal(expectedAccessToken.access_token); 67 | }); 68 | 69 | 70 | it('Get Refresh Token using Helper Method', () => { 71 | oauthClient.token.setToken(expectedAccessToken); 72 | const refreshToken = oauthClient.getToken().refreshToken(); 73 | 74 | expect(refreshToken).to.deep.equal(expectedAccessToken.refresh_token); 75 | }); 76 | 77 | it('Get TokenType using Helper Method', () => { 78 | oauthClient.token.setToken(expectedAccessToken); 79 | const tokenType = oauthClient.getToken().tokenType(); 80 | 81 | expect(tokenType).to.deep.equal(expectedAccessToken.token_type); 82 | }); 83 | 84 | it('Get Token using Helper Method', () => { 85 | oauthClient.token.setToken(expectedAccessToken); 86 | const token = oauthClient.getToken().getToken(); 87 | 88 | expect(token).to.be.a('Object'); 89 | expect(token.access_token).to.deep.equal('sample_access_token'); 90 | }); 91 | 92 | it('Clear Token using Helper Method', () => { 93 | oauthClient.token.setToken(expectedAccessToken); 94 | const token = oauthClient.getToken().clearToken(); 95 | 96 | expect(token.access_token).to.equal(''); 97 | expect(token.refresh_token).to.equal(''); 98 | expect(token.token_type).to.equal(''); 99 | expect(token.expires_in).to.equal(0); 100 | expect(token.x_refresh_token_expires_in).to.equal(0); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/mocks/authResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "token":{ 3 | "realmId":"sample_realm_id", 4 | "token_type":"bearer", 5 | "access_token":"sample_access_token", 6 | "refresh_token":"sample_refresh_token", 7 | "expires_in":1536130629063, 8 | "x_refresh_token_expires_in":1544853429063, 9 | "id_token":"sample_id_token", 10 | "latency":60000 11 | }, 12 | "response":{ 13 | "url":"https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer", 14 | "headers":{ 15 | "content-type":"application/json;charset=UTF-8", 16 | "content-length":"1636", 17 | "connection":"close", 18 | "server":"nginx", 19 | "date":"Wed, 05 Sep 2018 05:57:09 GMT", 20 | "intuit_tid":"1234-1234-1234-123", 21 | "cache-control":"no-cache, no-store", 22 | "pragma":"no-cache" 23 | }, 24 | "body":"{\"id_token\":\"sample_id_token\",\"expires_in\":3600,\"token_type\":\"bearer\",\"x_refresh_token_expires_in\":8726400,\"refresh_token\":\"sample_refresh_token\",\"access_token\":\"sample_access_token\"}", 25 | "status":200, 26 | "statusText":"OK" 27 | }, 28 | "body":"{\"id_token\":\"sample_id_token\",\"expires_in\":3600,\"token_type\":\"bearer\",\"x_refresh_token_expires_in\":8726400,\"refresh_token\":\"sample_refresh_token\",\"access_token\":\"sample_access_token\"}", 29 | "json":{ 30 | "id_token":"sample_id_token", 31 | "expires_in":3600, 32 | "token_type":"bearer", 33 | "x_refresh_token_expires_in":8726400, 34 | "refresh_token":"sample_refresh_token", 35 | "access_token":"sample_access_token" 36 | }, 37 | "intuit_tid":"1234-1234-1234-123" 38 | } -------------------------------------------------------------------------------- /test/mocks/bearer-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "sample_access_token", 3 | "refresh_token": "sample_refresh_token", 4 | "token_type": "bearer", 5 | "expires_in": "3600", 6 | "x_refresh_token_expires_in": "8726400" 7 | } 8 | -------------------------------------------------------------------------------- /test/mocks/errorResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": { 3 | "realmId": "123456789123", 4 | "token_type": "bearer", 5 | "access_token": "sample_access_token", 6 | "refresh_token": "sample_refresh_token", 7 | "expires_in": 0, 8 | "x_refresh_token_expires_in": 0, 9 | "id_token": "sample_id_token", 10 | "latency": 60000 11 | }, 12 | "response": { 13 | "url": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer", 14 | "headers": { 15 | "content-type": "application/json;charset=UTF-8", 16 | "content-length": "61", 17 | "connection": "close", 18 | "server": "nginx", 19 | "date": "Tue, 11 Sep 2018 07:52:23 GMT", 20 | "strict-transport-security": "max-age=15552000", 21 | "intuit_tid":"1234-1234-1234-123", 22 | "cache-control": "no-cache, no-store", 23 | "pragma": "no-cache" 24 | }, 25 | "body": "{\"error_description\":\"Token invalid\",\"error\":\"invalid_grant\"}", 26 | "status": 400, 27 | "statusText": "Bad Request" 28 | }, 29 | "body": "{\"error_description\":\"Token invalid\",\"error\":\"invalid_grant\"}", 30 | "json": { 31 | "error_description": "Token invalid", 32 | "error": "invalid_grant" 33 | }, 34 | "intuit_tid":"1234-1234-1234-123" 35 | } -------------------------------------------------------------------------------- /test/mocks/expectedValidateIDTokenCall.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/oauth-jsclient/28182fa8c242e243c3b8ef4f19c526bfda43675a/test/mocks/expectedValidateIDTokenCall.json -------------------------------------------------------------------------------- /test/mocks/jwkResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "body":"{\"keys\": [{\"kty\":\"sample_value_kty\",\"e\":\"sample_value_e\",\"use\":\"sample_value_use\",\"kid\":\"r4p5SbL2qaFehFzhj8gI\",\"alg\":\"sample_value_alg\",\"n\":\"sample_value_n\"}]}" 3 | } 4 | -------------------------------------------------------------------------------- /test/mocks/makeAPICallResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "CompanyInfo":{ 3 | "CompanyName":"Sample Test", 4 | "LegalName":"Sample Test", 5 | "CompanyAddr":{ 6 | "Id":"1", 7 | "Line1":"2565 Garcia Avenue", 8 | "City":"Mountain View", 9 | "Country":"US", 10 | "CountrySubDivisionCode":"CA", 11 | "PostalCode":"94063" 12 | }, 13 | "CustomerCommunicationAddr":{ 14 | "Id":"1", 15 | "Line1":"2565 Garcia Avenue", 16 | "City":"Mountain View", 17 | "Country":"US", 18 | "CountrySubDivisionCode":"CA", 19 | "PostalCode":"94063" 20 | }, 21 | "LegalAddr":{ 22 | "Id":"1", 23 | "Line1":"2565 Garcia Avenue", 24 | "City":"Mountain View", 25 | "Country":"US", 26 | "CountrySubDivisionCode":"CA", 27 | "PostalCode":"94063" 28 | }, 29 | "PrimaryPhone":{ 30 | 31 | }, 32 | "CompanyStartDate":"2018-03-03", 33 | "FiscalYearStartMonth":"January", 34 | "Country":"US", 35 | "Email":{ 36 | "Address":"developer.intuit.com@gmail.com" 37 | }, 38 | "WebAddr":{ 39 | 40 | }, 41 | "SupportedLanguages":"en", 42 | "NameValue":[ 43 | { 44 | "Name":"NeoEnabled", 45 | "Value":"true" 46 | }, 47 | { 48 | "Name":"IsQbdtMigrated", 49 | "Value":"false" 50 | }, 51 | { 52 | "Name":"SubscriptionStatus", 53 | "Value":"TRIAL" 54 | }, 55 | { 56 | "Name":"OfferingSku", 57 | "Value":"QuickBooks Online Plus" 58 | }, 59 | { 60 | "Name":"PayrollFeature", 61 | "Value":"true" 62 | }, 63 | { 64 | "Name":"AccountantFeature", 65 | "Value":"false" 66 | }, 67 | { 68 | "Name":"ItemCategoriesFeature", 69 | "Value":"true" 70 | }, 71 | { 72 | "Name":"AssignedTime", 73 | "Value":"2018-03-05T19:38:07-08:00" 74 | } 75 | ], 76 | "domain":"QBO", 77 | "sparse":false, 78 | "Id":"1", 79 | "SyncToken":"21", 80 | "MetaData":{ 81 | "CreateTime":"2018-03-03T01:05:25-08:00", 82 | "LastUpdatedTime":"2018-08-23T10:46:22-07:00" 83 | } 84 | }, 85 | "time":"2018-09-04T16:53:59.490-07:00" 86 | } -------------------------------------------------------------------------------- /test/mocks/openID-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "sample_access_token", 3 | "refresh_token": "sample_refresh_token", 4 | "token_type": "bearer", 5 | "expires_in": "3600", 6 | "x_refresh_token_expires_in": "8726400", 7 | "id_token": "eyJraWQiOiJyNHA1U2JMMnFhRmVoRnpoajhnSSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJiMDUzZDk5NC0wN2Q1LTQ2OGQtYjdlZS0yMmUzNDlkMmU3MzkiLCJhdWQiOlsiTDM5ZWxTdWJGeGpQT1NwZFpvWVdSS2lDQ0U2VElOanY2N1JvYUU4ekJxYkl4eGI0bEsiXSwicmVhbG1pZCI6IjExMDgwMzM0NzEiLCJhdXRoX3RpbWUiOjE0NjI1NTQ0NzUsImlzcyI6Imh0dHBzOlwvXC9vYXV0aC1lMmUucGxhdGZvcm0uaW50dWl0LmNvbVwvb2F1dGgyXC92MVwvb3BcL3YxIiwiZXhwIjoxNDYyNTYxMzI4LCJpYXQiOjE0NjI1NTc3Mjh9.BIJ9x_WPEOZsLJfQE3mGji_Q15j_rdlTyFYELiJM-W92fWSLC-TLEwCp5IrRhDWMvyvrLSMZCEdQALYQpbVy8uKI22JgGWYvkwNEDweOjbYzyt33F4xtn3GGcW9nAwRtA3M19qquWyi7G0kcCZUDN8RfUXz2qKMJ6KPOfLVe2UQ" 8 | } 9 | -------------------------------------------------------------------------------- /test/mocks/pdfResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "url":"https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer", 3 | "headers":{ 4 | "content-type":"application/pdf", 5 | "content-length":"1636", 6 | "connection":"close", 7 | "server":"nginx", 8 | "date":"Wed, 05 Sep 2018 05:57:09 GMT", 9 | "intuit_tid":"1234-1234-1234-123", 10 | "cache-control":"no-cache, no-store", 11 | "pragma":"no-cache" 12 | }, 13 | "body":"%PDF-1.\ntrailer<>]>>>>>>", 14 | "status":200, 15 | "statusText":"OK" 16 | } 17 | -------------------------------------------------------------------------------- /test/mocks/refreshResponse.json: -------------------------------------------------------------------------------- 1 | "{\"access_token\":\"sample_access_token_2\",\"token_type\":\"bearer\",\"x_refresh_token_expires_in\":\"8726400\",\"refresh_token\":\"sample_refresh_token\",\"expires_in\":\"3600\",\"id_token\":\"sample_id_token\"}" -------------------------------------------------------------------------------- /test/mocks/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "url":"https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer", 3 | "headers":{ 4 | "content-type":"application/json;charset=UTF-8", 5 | "content-length":"1636", 6 | "connection":"close", 7 | "server":"nginx", 8 | "date":"Wed, 05 Sep 2018 05:57:09 GMT", 9 | "strict-transport-security":"max-age=15552000", 10 | "intuit_tid":"1234-1234-1234-123", 11 | "cache-control":"no-cache, no-store", 12 | "pragma":"no-cache" 13 | }, 14 | "body":"{\"id_token\":\"sample_id_token\",\"expires_in\":3600,\"token_type\":\"bearer\",\"x_refresh_token_expires_in\":8726400,\"refresh_token\":\"sample_refresh_token\",\"access_token\":\"sample_access_token\"}", 15 | "status":200, 16 | "statusText":"OK" 17 | } -------------------------------------------------------------------------------- /test/mocks/tokenResponse.json: -------------------------------------------------------------------------------- 1 | 2 | "{\"access_token\":\"sample_access_token\",\"token_type\":\"bearer\",\"x_refresh_token_expires_in\":\"8726400\",\"refresh_token\":\"sample_refresh_token\",\"expires_in\":\"3600\",\"id_token\":\"sample_id_token\"}" 3 | -------------------------------------------------------------------------------- /test/mocks/userInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "sub": "1182d6ec-2a1f-4aa3-af3f-bb3b95db45af", 3 | "email": "john@doe.com", 4 | "emailVerified": true, 5 | "givenName": "John", 6 | "familyName": "Doe" 7 | } -------------------------------------------------------------------------------- /test/mocks/validateIdToken.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "sub":"12345-76d0-4616-9905-e416279ea55e", 4 | "aud":["Q0U5kTPRgklmWD0WS8x5L4JMv2IDC2nxXNJzdXdlQ8LDrmNhAi"], 5 | "realmid":"123456789123", 6 | "auth_time":1536131119, 7 | "iss":"https://oauth.platform.intuit.com/op/v1", 8 | "exp":1536137232, 9 | "iat":1536133632 10 | } 11 | -------------------------------------------------------------------------------- /views/SDK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/oauth-jsclient/28182fa8c242e243c3b8ef4f19c526bfda43675a/views/SDK.png --------------------------------------------------------------------------------