├── .eslintrc ├── .github └── workflows │ ├── dry-publish.js.yml │ ├── publish.js.yml │ └── quality.js.yml ├── .gitignore ├── .nycrc ├── .releaserc ├── .secrets.baseline ├── LICENSE ├── README.md ├── lib ├── appid-sdk.js ├── data │ └── languagesCodesArray.json ├── self-service │ └── self-service-manager.js ├── strategies │ ├── api-strategy.js │ └── webapp-strategy.js ├── token-manager │ └── token-manager.js ├── types │ ├── appid-sdk.d.ts │ └── appid-sdk.test-d.ts ├── user-profile-manager │ ├── unauthorized-exception.js │ └── user-profile-manager.js └── utils │ ├── common-util.js │ ├── constants.js │ ├── public-key-util.js │ ├── request-util.js │ ├── service-util.js │ └── token-util.js ├── log4js.json ├── package.json ├── public └── index.html ├── samples ├── api-app-sample-server.js ├── browser-app-sample.html ├── browser-app-sample.js ├── cloud-directory-app-sample-server.js ├── cloud-directory-app-sample.html ├── custom-identity-app-sample-server.js ├── images │ ├── bg.png │ └── logo_cloud_land.png ├── index.html ├── resources │ └── private.pem ├── translations │ ├── en.json │ └── fr.json ├── views │ ├── account_confirmed.ejs │ ├── cd_login.ejs │ ├── change_details.ejs │ ├── change_password.ejs │ ├── custom_identity_login.ejs │ ├── login.ejs │ ├── reset_password_expired.ejs │ ├── reset_password_form.ejs │ ├── reset_password_sent.ejs │ ├── reset_password_success.ejs │ ├── self_forgot_password.ejs │ ├── self_sign_up.ejs │ └── thanks_for_sign_up.ejs ├── web-app-sample-server.js ├── web-app-sample.html └── web-app-sample.js └── test ├── api-strategy-test.js ├── appid-sdk-test.js ├── common-util-test.js ├── mocks ├── constants.js ├── public-key-util-mock.js ├── request-mock.js └── token-util-mock.js ├── public-key-util-test.js ├── request-util-test.js ├── self-service-manager-test.js ├── service-util-test.js ├── token-manager-test.js ├── token-util-test.js ├── user-profile-manager-test.js └── webapp-strategy-test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 2017, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "browser": false, 9 | "node": true, 10 | "es6": true 11 | }, 12 | 13 | "plugins": [ 14 | "node" 15 | ], 16 | "extends": [ 17 | "airbnb-base", 18 | "eslint:recommended", 19 | "plugin:node/recommended" 20 | ], 21 | "rules": { 22 | "max-len": [ 23 | 2, 24 | 130, 25 | 4 26 | ], 27 | "curly": [ 28 | 2, 29 | "all" 30 | ], 31 | "operator-linebreak": [ 32 | 2, 33 | "after" 34 | ], 35 | "indent": [ 36 | 0, 37 | "tab", 38 | { 39 | "SwitchCase": 1 40 | } 41 | ], 42 | "quotes": [ 43 | 0, 44 | "single" 45 | ], 46 | "no-mixed-spaces-and-tabs": 0, 47 | "space-unary-ops": [ 48 | 2, 49 | { 50 | "nonwords": false, 51 | "overrides": {} 52 | } 53 | ], 54 | "keyword-spacing": [ 55 | 2, 56 | {} 57 | ], 58 | "space-infix-ops": 0, 59 | "space-before-blocks": [ 60 | 2, 61 | "always" 62 | ], 63 | "array-bracket-spacing": [ 64 | 0, 65 | "never", 66 | { 67 | "singleValue": true 68 | } 69 | ], 70 | "space-in-parens": [ 71 | 2, 72 | "never" 73 | ], 74 | "no-multiple-empty-lines": 2, 75 | "wrap-iife": 2, 76 | "valid-jsdoc": 2, 77 | "no-param-reassign": [ 78 | 2, 79 | { 80 | "props": false 81 | } 82 | ], 83 | "no-plusplus": 0, 84 | "no-underscore-dangle": 0, 85 | "prefer-rest-params": 0, 86 | "comma-dangle": [ 87 | "error", 88 | "never" 89 | ], 90 | "semi": 2, 91 | "no-console": 2, 92 | "no-irregular-whitespace": 1, 93 | "no-sparse-arrays": 1, 94 | "object-curly-spacing": "off", 95 | "no-tabs": 0, 96 | "no-unreachable": 1, 97 | "valid-typeof": 1, 98 | "no-shadow": 1, 99 | 100 | "no-path-concat": 1, 101 | "eqeqeq": [ 102 | 2, 103 | "smart" 104 | ], 105 | "no-use-before-define": 1, 106 | "no-new-require": 1, 107 | // "no-undef": 2, 108 | 109 | 110 | "no-mixed-requires": 0, 111 | "no-multi-spaces": 0, 112 | // "no-unused-vars": [2, {"vars": "all", "args": "none"}], 113 | "dot-notation": 0, 114 | "new-cap": 0, 115 | "one-var": 0, 116 | "handle-callback-err": [ 117 | 2, 118 | "^.*(e|E)rr" 119 | ], 120 | "node/no-unsupported-features/es-syntax": [1, {//warn 121 | "version": ">=8.11.0", 122 | "ignores": [] 123 | }] 124 | }, 125 | "globals": { 126 | "__dirname": true, 127 | "after": true, 128 | "afterEach": true, 129 | "assert": true, 130 | "before": true, 131 | "browser": true, 132 | "Buffer": true, 133 | "by": true, 134 | "console": true, 135 | "describe": true, 136 | "element": true, 137 | "emit": true, 138 | "it": true, 139 | "module": true, 140 | "process": true, 141 | "require": true, 142 | "setTimeout": true, 143 | "transclude": true, 144 | "templateUrl": true, 145 | "cwd": true, 146 | "__base": true 147 | } 148 | } -------------------------------------------------------------------------------- /.github/workflows/dry-publish.js.yml: -------------------------------------------------------------------------------- 1 | # The objective of the dry-run job is to get a preview of the pending release. 2 | # Dry-run mode skips the following steps: prepare, publish, success and fail. 3 | # In addition to this it prints the next version and release notes to the console. 4 | 5 | # Note: The Dry-run mode verifies the repository push permission, even though nothing will be pushed. 6 | # The verification is done to help user to figure out potential configuration issues. 7 | name: Dry Publish 8 | 9 | 10 | on: 11 | workflow_dispatch: 12 | 13 | jobs: 14 | dry-publish: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [18.x] 19 | if: ${{ github.ref == 'refs/heads/master' }} 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npx -p @semantic-release/exec -p semantic-release semantic-release --dry-run --plugins "@semantic-release/commit-analyzer,@semantic-release/exec" --analyzeCommits @semantic-release/commit-analyzer --verifyRelease @semantic-release/exec --verifyReleaseCmd 'echo ${nextRelease.version}' 28 | env: 29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node and then publish the new bundle to NPM 2 | 3 | name: Publish CD 4 | 5 | 6 | on: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | quality: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [18.x] 15 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm install -g codecov 24 | - run: npm run coverage 25 | 26 | publish: 27 | runs-on: ubuntu-latest 28 | if: ${{ github.ref == 'refs/heads/master' }} 29 | needs: [quality] 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v2 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | - run: npm install 37 | - run: npm run semantic-release 38 | env: 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/quality.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | 3 | name: Quality CI 4 | 5 | on: 6 | push: 7 | branches: [ development, master ] 8 | pull_request: 9 | branches: [ development, master ] 10 | 11 | jobs: 12 | quality: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: 12 21 | - run: npm install 22 | - run: npm install -g codecov 23 | - run: npm run coverage 24 | 25 | - name: Coveralls 26 | uses: coverallsapp/github-action@master 27 | with: 28 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like nyc 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .idea 39 | package-lock.json -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "lines": 80, 3 | "statements": 80, 4 | "functions": 80, 5 | "branches": 60, 6 | "reporter": [ 7 | "cobertura", 8 | "text", 9 | "text-summary", 10 | "lcov" 11 | ], 12 | "include": [ 13 | "lib/**/*.js" 14 | ], 15 | "all": true, 16 | "check-coverage": false, 17 | "per-file": false, 18 | "report-dir": "./coverage", 19 | "cache": false 20 | } 21 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "${version}" 3 | } -------------------------------------------------------------------------------- /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 {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/appid-sdk.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const path = require("path"); 15 | const Log4js = require("log4js"); 16 | const logger = Log4js.getLogger("appid-sdk"); 17 | 18 | if (!process.env.LOG4JS_CONFIG) { 19 | Log4js.configure(path.join(__dirname + "/../log4js.json")); 20 | } 21 | 22 | logger.info("Initialized"); 23 | const APIStrategy = require("./strategies/api-strategy"); 24 | const WebAppStrategy = require("./strategies/webapp-strategy"); 25 | const UserProfileManager = require("./user-profile-manager/user-profile-manager"); 26 | const SelfServiceManager = require("./self-service/self-service-manager"); 27 | const UnauthorizedException = require("./user-profile-manager/unauthorized-exception"); 28 | const TokenManager = require("./token-manager/token-manager"); 29 | 30 | module.exports = { 31 | APIStrategy: APIStrategy, 32 | WebAppStrategy: WebAppStrategy, 33 | SelfServiceManager: SelfServiceManager, 34 | UserProfileManager: UserProfileManager, 35 | UnauthorizedException: UnauthorizedException, 36 | TokenManager: TokenManager 37 | }; 38 | -------------------------------------------------------------------------------- /lib/data/languagesCodesArray.json: -------------------------------------------------------------------------------- 1 | [ 2 | "af","af-ZA", 3 | "sq","sq-AL", 4 | "am","am-ET", 5 | "ar","ar-DZ","ar-BH","ar-EG","ar-IQ","ar-JO","ar-KW","ar-LB","ar-LY","ar-MR","ar-MA","ar-OM","ar-QA","ar-SA","ar-SY","ar-TN","ar-AE","ar-YE", 6 | "hy","hy-AM", 7 | "as","as-IN", 8 | "az","az-AZ", 9 | "eu","eu-ES", 10 | "be","be-BY", 11 | "bn","bn-BD","bn-IN", 12 | "bs","bs-Latn-BA", 13 | "bg","bg-BG", 14 | "my","my-MM", 15 | "ca","ca-ES", 16 | "zh","zh-Hans-CN","zh-Hans-SG","zh-Hant-HK","zh-Hant-MO","zh-Hant-TW", 17 | "hr","hr-HR", 18 | "cs","cs-CZ", 19 | "da","da-DK", 20 | "nl","nl-BE","nl-NL", 21 | "en","en-AU","en-BE","en-CM","en-CA","en-GH","en-HK","en-IN","en-IE","en-KE","en-MU","en-NZ","en-NG","en-PH","en-SG","en-ZA","en-TZ","en-GB","en-US","en-ZM", 22 | "et","et-EE", 23 | "fil","fil-PH", 24 | "fi","fi-FI", 25 | "fr","fr-DZ","fr-CM","fr-CD","fr-BE","fr-CA","fr-FR","fr-CI","fr-LU","fr-MR","fr-MU","fr-MA","fr-SN","fr-CH","fr-TN", 26 | "gl","gl-ES", 27 | "lg","lg-UG", 28 | "ka","ka-GE", 29 | "de","de-AT","de-DE","de-LU","de-CH", 30 | "el","el-GR", 31 | "gu","gu-IN", 32 | "ha","ha-NG", 33 | "he","he-IL", 34 | "hi","hi-IN", 35 | "hu","hu-HU", 36 | "is","is-IS", 37 | "ig","ig-NG", 38 | "id","id-ID", 39 | "it","it-IT","it-CH", 40 | "ja","ja-JP", 41 | "kn","kn-IN", 42 | "kk","kk-KZ", 43 | "km","km-KH", 44 | "rw","rw-RW", 45 | "kok","kok-IN", 46 | "ko","ko-KR", 47 | "lo","lo-LA", 48 | "lv","lv-LV", 49 | "lt","lt-LT", 50 | "mk","mk-MK", 51 | "ms","ms-Latn-MY", 52 | "ml","ml-IN", 53 | "mt","mt-MT", 54 | "mr","mr-IN", 55 | "mn","mn-Cyrl-MN", 56 | "ne","ne-IN","ne-NP", 57 | "nb","nb-NO", 58 | "nn","nn-NO", 59 | "or","or-IN", 60 | "om","om-ET", 61 | "pl","pl-PL", 62 | "pt","pt-AO","pt-BR","pt-MO","pt-MZ","pt-PT", 63 | "pa","pa-IN", 64 | "ro","ro-RO", 65 | "ru","ru-RU", 66 | "sr","sr-Cyrl-RS","sr-Latn-ME","sr-Latn-RS", 67 | "si","si-LK", 68 | "sk","sk-SK", 69 | "sl","sl-SI", 70 | "es","es-AR","es-BO","es-CL","es-CO","es-CR","es-DO","es-EC","es-SV","es-GT","es-HN","es-MX","es-NI","es-PA","es-PY","es-PE","es-PR","es-ES","es-US","es-UY","es-VE", 71 | "sw","sw-KE","sw-TZ", 72 | "sv","sv-SE", 73 | "ta","ta-IN", 74 | "te","te-IN", 75 | "th","th-TH", 76 | "tr","tr-TR", 77 | "uk","uk-UA", 78 | "ur","ur-IN","ur-PK", 79 | "uz","uz-Cyrl-UZ","uz-Latn-UZ", 80 | "vi","vi-VN", 81 | "cy","cy-GB", 82 | "yo","yo-NG", 83 | "zu","zu-ZA" 84 | ] -------------------------------------------------------------------------------- /lib/strategies/api-strategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const log4js = require("log4js"); 15 | const constants = require('../utils/constants'); 16 | const TokenUtil = require("../utils/token-util"); 17 | const ServiceUtil = require('../utils/service-util'); 18 | const PublicKeyUtil = require("../utils/public-key-util"); 19 | 20 | const ERROR = { 21 | INVALID_REQUEST: "invalid_request", // HTTP 400 22 | INVALID_TOKEN: "invalid_token", // HTTP 401 23 | INSUFFICIENT_SCOPE: "insufficient_scope" // HTTP 401 24 | }; 25 | 26 | const AUTHORIZATION_HEADER = "Authorization"; 27 | const STRATEGY_NAME = "appid-api-strategy"; 28 | const BEARER = "Bearer"; 29 | 30 | const logger = log4js.getLogger(STRATEGY_NAME); 31 | 32 | function ApiStrategy(options) { 33 | logger.debug("Initializing"); 34 | options = options || {}; 35 | this.name = ApiStrategy.STRATEGY_NAME; 36 | this.serviceConfig = new ServiceUtil.loadConfig('APIStrategy', [constants.OAUTH_SERVER_URL], options); 37 | } 38 | 39 | ApiStrategy.STRATEGY_NAME = STRATEGY_NAME; 40 | ApiStrategy.DEFAULT_SCOPE = "appid_default"; 41 | 42 | /** 43 | * 44 | * @param req - an HTTP request object 45 | * @param options.scope - The required scopes, separated by spaces. For example: 'read write update' 46 | * @param options.audience - (optional) the application clientId, or the resource URI. 47 | * @returns {*} 48 | */ 49 | ApiStrategy.prototype.authenticate = function (req, options = {}) { 50 | var self = this; 51 | logger.debug("authenticate"); 52 | 53 | if (options.scope && typeof options.scope !== 'string' || options.audience && typeof options.audience !== 'string') { 54 | return self.fail(buildWwwAuthenticateHeader('Illegal Scope', ERROR.INVALID_REQUEST), 400); 55 | } 56 | 57 | let requiredScopes = ApiStrategy.DEFAULT_SCOPE; 58 | if (options.scope && options.scope.trim()) { // if the required scopes are just whitespace, skip. 59 | // split by spaces and keep only non empty scopes. excluded default scope as it is always there 60 | const scopesArray = options.scope.split(" ").filter(scope => scope !== '' && scope !== ApiStrategy.DEFAULT_SCOPE); 61 | for (const requiredScope of scopesArray) { 62 | requiredScopes += " " + requiredScope; 63 | } 64 | } 65 | 66 | // Retrieve authorization header from request 67 | const authHeader = req.header(AUTHORIZATION_HEADER); 68 | if (!authHeader) { 69 | logger.warn("Authorization header not found"); 70 | return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401); 71 | } 72 | 73 | // Validate that first header component is Bearer 74 | const authHeaderComponents = authHeader.split(" "); 75 | if (authHeaderComponents[0].indexOf(BEARER) !== 0) { 76 | logger.warn("Malformed authorization header"); 77 | return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401); 78 | } 79 | 80 | // Validate header has exactly 2 or 3 components (Bearer, access_token, [id_token]) 81 | if (authHeaderComponents.length !== 2 && authHeaderComponents.length !== 3) { 82 | logger.warn("Malformed authorization header"); 83 | return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401); 84 | } 85 | 86 | // Validate second header component is a valid access_token 87 | var accessTokenString = authHeaderComponents[1]; 88 | 89 | // Decode and validate access_token 90 | TokenUtil.decodeAndValidate(accessTokenString, this.serviceConfig.getOAuthServerUrl()).then(function (accessToken) { 91 | if (!accessToken) { 92 | logger.warn("Invalid access_token"); 93 | return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401); 94 | } 95 | // Validate token contains required scopes 96 | const requiredScopesArray = requiredScopes.split(" ").filter(scope => scope !== ""); 97 | const suppliedScopesArray = accessToken.scope.split(" "); 98 | for (const requiredScope of requiredScopesArray) { 99 | if (!suppliedScopesArray.includes(requiredScope)) { 100 | logger.warn("access_token does not contain required scope. Expected ::", requiredScopes, " Received ::", 101 | accessToken.scope); 102 | return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INSUFFICIENT_SCOPE), 401); 103 | } 104 | } 105 | 106 | // validate the audience section 107 | if (!accessToken.aud) { 108 | return self.fail(buildWwwAuthenticateHeader("access token missing audience section", ERROR.INVALID_TOKEN), 401); 109 | } 110 | if (!Array.isArray(accessToken.aud)) { 111 | return self.fail(buildWwwAuthenticateHeader("access token malformed audience array", ERROR.INVALID_TOKEN), 401); 112 | } 113 | if (options.audience && options.audience.trim()) { 114 | // audience is an array (currently we support only 1 item in the array) 115 | const requestAudience = options.audience.trim(); 116 | const requiredList = requestAudience.split(' '); 117 | if (requiredList.length > 1) { 118 | return self.fail(buildWwwAuthenticateHeader("multiple audiences are not supported", ERROR.INVALID_REQUEST), 400); 119 | } 120 | const tokenAudience = accessToken.aud; 121 | if (!tokenAudience.includes(requestAudience)) { 122 | return self.fail(buildWwwAuthenticateHeader("audience mismatch. expected:" + tokenAudience + 123 | " got:" + requestAudience, ERROR.INSUFFICIENT_SCOPE), 401); 124 | } 125 | } 126 | 127 | req.appIdAuthorizationContext = { 128 | accessToken: accessTokenString, 129 | accessTokenPayload: accessToken 130 | }; 131 | 132 | // Decode and validate id_token 133 | var identityTokenString; 134 | var identityToken; 135 | if (authHeaderComponents.length === 3) { 136 | identityTokenString = authHeaderComponents[2]; 137 | TokenUtil.decodeAndValidate(identityTokenString, self.serviceConfig.getOAuthServerUrl()).then(function (identityToken) { 138 | if (identityToken) { 139 | req.appIdAuthorizationContext.identityToken = identityTokenString; 140 | req.appIdAuthorizationContext.identityTokenPayload = identityToken; 141 | } else { 142 | logger.warn("Invalid identity_token. Proceeding with access_token only"); 143 | } 144 | logger.debug("authentication success"); 145 | return self.success(identityToken || null); 146 | }).catch(() => { 147 | logger.debug("authentication failed due to invalid identity token"); 148 | return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401); 149 | }); 150 | } else { 151 | logger.debug("authentication success: identity_token not found. Proceeding with access_token only"); 152 | return self.success(identityToken || null); 153 | } 154 | }).catch(function () { 155 | logger.debug("authentication failed due to invalid access token"); 156 | return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401); 157 | }); 158 | 159 | // .success(user, info) - call on auth success. user=object, info=object 160 | // .fail(challenge, status) - call on auth failure. challenge=string, status=int 161 | // .redirect(url, status) - call on redirect required. url=url, status=int 162 | // .pass() - skip strategy processing 163 | // .error(err) - error during strategy processing. err=Error obj 164 | }; 165 | 166 | function buildWwwAuthenticateHeader(scope, error) { 167 | var msg = BEARER + " scope=\"" + scope + "\""; 168 | if (error) { 169 | msg += ", error=\"" + error + "\""; 170 | } 171 | return msg; 172 | } 173 | 174 | module.exports = ApiStrategy; 175 | -------------------------------------------------------------------------------- /lib/token-manager/token-manager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const log4js = require("log4js"); 15 | const logger = log4js.getLogger("appid-token-manager"); 16 | const request = require('../utils/request-util'); 17 | 18 | const constants = require('../utils/constants'); 19 | const TokenUtil = require("../utils/token-util"); 20 | const ServiceUtil = require('../utils/service-util'); 21 | 22 | 23 | function TokenManager(options) { 24 | logger.debug("Initializing token manager"); 25 | this.serviceConfig = new ServiceUtil.loadConfig('TokenManager', [ 26 | constants.CLIENT_ID, 27 | constants.SECRET, 28 | constants.OAUTH_SERVER_URL 29 | ], options); 30 | } 31 | 32 | TokenManager.prototype.getCustomIdentityTokens = function (jwsToken, scopes = []) { 33 | const clientId = this.serviceConfig.getClientId(); 34 | const secret = this.serviceConfig.getSecret(); 35 | const scope = scopes.join(' '); 36 | const tokenEndpoint = this.serviceConfig.getOAuthServerUrl() + constants.TOKEN_PATH; 37 | 38 | 39 | return getTokens(tokenEndpoint, clientId, secret, constants.CUSTOM_IDENTITY_GRANT_TYPE, scope, jwsToken) 40 | .then((tokenResponse) => { 41 | if (!tokenResponse) { 42 | logger.error('Unable to parse token response'); 43 | throw Error('Unable to parse token response'); 44 | } 45 | 46 | const accessToken = tokenResponse['access_token']; 47 | const identityToken = tokenResponse['id_token']; 48 | const tokenType = tokenResponse['token_type']; 49 | const expiresIn = tokenResponse['expires_in']; 50 | 51 | return Promise.all([ 52 | validateToken('access', accessToken, this.serviceConfig), 53 | validateToken('identity', identityToken, this.serviceConfig) 54 | ]).then(() => ({ 55 | accessToken, 56 | identityToken, 57 | tokenType, 58 | expiresIn 59 | })); 60 | }); 61 | }; 62 | 63 | TokenManager.prototype.getApplicationIdentityToken = function () { 64 | 65 | const clientId = this.serviceConfig.getClientId(); 66 | const secret = this.serviceConfig.getSecret(); 67 | const tokenEndPoint = this.serviceConfig.getOAuthServerUrl() + constants.TOKEN_PATH; 68 | 69 | return getTokens(tokenEndPoint, clientId, secret, constants.APP_TO_APP_GRANT_TYPE) 70 | .then((tokenResponse) => { 71 | if (!tokenResponse) { 72 | logger.error('Unable to parse token response'); 73 | throw Error('Unable to parse token response'); 74 | } 75 | 76 | const accessToken = tokenResponse['access_token']; 77 | const tokenType = tokenResponse['token_type']; 78 | const expiresIn = tokenResponse['expires_in']; 79 | 80 | return validateToken('access', accessToken, this.serviceConfig) 81 | .then(() => ({ 82 | accessToken, 83 | tokenType, 84 | expiresIn 85 | })); 86 | }); 87 | }; 88 | 89 | function getTokens(tokenEndpoint, clientId, secret, grant_type, scope, assertion) { 90 | return new Promise((resolve, reject) => { 91 | request({ 92 | method: 'POST', 93 | url: tokenEndpoint, 94 | auth: { 95 | username: clientId, 96 | password: secret 97 | }, 98 | form: { 99 | grant_type, 100 | scope, 101 | assertion 102 | } 103 | }, (error, response, body) => { 104 | if (error) { 105 | logger.error('Failed to obtain tokens. Error: ', error); 106 | reject(error); 107 | } else if (response.statusCode === 200) { 108 | resolve(body); 109 | } else { 110 | if (response.statusCode === 401 || response.statusCode === 403) { 111 | logger.error('Unauthorized. Error: ', body.error_description); 112 | reject(new Error('Unauthorized')); 113 | } else if (response.statusCode === 404) { 114 | logger.error('Not found'); 115 | reject(new Error("Not found")); 116 | } else if (response.statusCode === 400) { 117 | logger.error('Failed to obtain tokens. Error: ', body.error_description); 118 | reject(new Error('Failed to obtain tokens')); 119 | } else { 120 | logger.error(body); 121 | reject(new Error("Unexpected error")); 122 | } 123 | } 124 | }); 125 | }); 126 | } 127 | 128 | function validateToken(tokenType, token, serviceConfig) { 129 | logger.debug(`Validating ${tokenType} token`); 130 | return TokenUtil.decodeAndValidate(token, serviceConfig.getOAuthServerUrl()).then((validatedToken) => { 131 | if (!validatedToken) { 132 | logger.error(`Invalid ${tokenType} token`); 133 | throw Error(`Invalid ${tokenType} token`); 134 | } 135 | 136 | return TokenUtil.validateIssAndAud(validatedToken, serviceConfig).catch(error => { 137 | logger.error(error); 138 | throw Error(`${tokenType} token has invalid claims`); 139 | }); 140 | }); 141 | } 142 | 143 | module.exports = TokenManager; -------------------------------------------------------------------------------- /lib/types/appid-sdk.d.ts: -------------------------------------------------------------------------------- 1 | // Author : Younes A 2 | 3 | export interface StrategyOptions { 4 | [key: string]: any; 5 | } 6 | 7 | export interface SelfServiceOptions { 8 | iamApiKey?: string; 9 | managementUrl?: string; 10 | tenantId?: string; 11 | oAuthServerUrl?: string; 12 | iamTokenUrl?: string; 13 | } 14 | 15 | export class Strategy { 16 | authenticate: () => void; 17 | } 18 | 19 | export interface ApplicationIdentityToken { 20 | accessToken: string; 21 | tokenType: string; 22 | expiresIn: number; 23 | } 24 | 25 | export interface CustomIdentityToken extends ApplicationIdentityToken { 26 | identityToken: string; 27 | } 28 | 29 | export interface UserSCIM { 30 | id: string; 31 | userName: string; 32 | [key: string]: any; 33 | } 34 | 35 | // tslint:disable-next-line:no-unnecessary-class 36 | export class APIStrategy extends Strategy { 37 | constructor(options: StrategyOptions); 38 | } 39 | 40 | // tslint:disable-next-line:no-unnecessary-class 41 | export class WebAppStrategy extends Strategy { 42 | constructor(options: StrategyOptions); 43 | } 44 | 45 | // tslint:disable-next-line:no-unnecessary-class 46 | export class TokenManager { 47 | constructor(options: StrategyOptions); 48 | getApplicationIdentityToken: () => Promise; 49 | getCustomIdentityTokens: () => Promise; 50 | } 51 | 52 | // tslint:disable-next-line:no-unnecessary-class 53 | export class SelfServiceManager { 54 | constructor(options: SelfServiceOptions); 55 | signUp: ( 56 | userData: unknown, 57 | language: string, 58 | iamToken: string 59 | ) => Promise; 60 | } 61 | 62 | // tslint:disable-next-line:no-unnecessary-class 63 | export class UserProfileManager { 64 | constructor(); 65 | } 66 | 67 | // tslint:disable-next-line:no-unnecessary-class 68 | export class UnauthorizedException { 69 | constructor(); 70 | } -------------------------------------------------------------------------------- /lib/types/appid-sdk.test-d.ts: -------------------------------------------------------------------------------- 1 | // Author : Younes A 2 | import { 3 | APIStrategy, 4 | ApplicationIdentityToken, 5 | SelfServiceManager, 6 | Strategy, 7 | TokenManager, 8 | UserSCIM, 9 | WebAppStrategy, 10 | } from "./appid-sdk"; 11 | import { expectType } from "tsd"; 12 | 13 | expectType( 14 | new APIStrategy({ 15 | oauthServerUrl: "{oauth-server-url}", 16 | }) 17 | ); 18 | 19 | expectType( 20 | new WebAppStrategy({ 21 | tenantId: "{tenant-id}", 22 | clientId: "{client-id}", 23 | secret: "{secret}", 24 | oauthServerUrl: "{oauth-server-url}", 25 | redirectUri: "{app-url}" + "CALLBACK_URL", 26 | }) 27 | ); 28 | 29 | const config = { 30 | tenantId: "{tenant-id}", 31 | clientId: "{client-id}", 32 | secret: "{secret}", 33 | oauthServerUrl: "{oauth-server-url}", 34 | }; 35 | 36 | const tokenManager = new TokenManager(config); 37 | expectType( 38 | await tokenManager.getApplicationIdentityToken() 39 | ); 40 | 41 | const selfServiceManager = new SelfServiceManager({ 42 | iamApiKey: "{iam-api-key}", 43 | managementUrl: "{management-url}", 44 | }); 45 | 46 | const userData = { 47 | id: "2819c223-7f76-453a-919d-413861904646", 48 | externalId: "701984", 49 | userName: "bjensen@example.com", 50 | }; 51 | 52 | expectType( 53 | await selfServiceManager.signUp(userData, "en", "iamToken") 54 | ); -------------------------------------------------------------------------------- /lib/user-profile-manager/unauthorized-exception.js: -------------------------------------------------------------------------------- 1 | module.exports = function UnauthorizedException() { 2 | 3 | }; -------------------------------------------------------------------------------- /lib/user-profile-manager/user-profile-manager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const log4js = require("log4js"); 15 | const request = require('../utils/request-util'); 16 | const Q = require("q"); 17 | const _ = require("underscore"); 18 | const UnauthorizedException = require("./unauthorized-exception"); 19 | const Utils = require("../utils/token-util"); 20 | const constants = require('../utils/constants'); 21 | const logger = log4js.getLogger("appid-user-manager"); 22 | 23 | const VCAP_SERVICES = "VCAP_SERVICES"; 24 | const VCAP_SERVICES_CREDENTIALS = "credentials"; 25 | const VCAP_SERVICES_SERVICE_NAME1 = "AdvancedMobileAccess"; 26 | const VCAP_SERVICES_SERVICE_NAME2 = "AppID"; 27 | const ATTRIBUTES_ENDPOINT = "/api/v1/attributes"; 28 | const USERINFO_ENDPOINT = "/userinfo"; 29 | 30 | const serviceUtils = require('../utils/service-util'); 31 | 32 | function UserProfileManager() {} 33 | 34 | /** 35 | * Initialize user profile manager 36 | * @param {Object} options The options object initializes the object with specific settings, If you are uploading the application to IBM cloud, those credentials can be read from IBM cloud and you can initialize this object without any options 37 | * @param {string} options.appidServiceEndpoint appid's service endpoint 38 | * @param {string} options.version appid's server version in a number format (3 OR 4) 39 | * @param {string} options.tenantId your application tenantId 40 | * @param {string} options.oauthServerUrl appid's server url- needs to be provided if service endpoint isn't provided 41 | * @param {string} options.profilesUrl appid's user profile url - needs to be provided if service endpoint isn't provided 42 | */ 43 | UserProfileManager.prototype.init = function (options) { // eslint-disable-line complexity 44 | options = options || {}; 45 | const vcapServices = JSON.parse(process.env[VCAP_SERVICES] || "{}"); 46 | var vcapServiceCredentials = {}; 47 | 48 | // Find App ID service config 49 | for (var propName in vcapServices) { 50 | // Does service name starts with VCAP_SERVICES_SERVICE_NAME 51 | if (propName.indexOf(VCAP_SERVICES_SERVICE_NAME1) === 0 || propName.indexOf(VCAP_SERVICES_SERVICE_NAME2) === 0) { 52 | vcapServiceCredentials = vcapServices[propName][0][VCAP_SERVICES_CREDENTIALS]; 53 | break; 54 | } 55 | } 56 | try { 57 | const config = serviceUtils.loadConfig("user profile config", [constants.OAUTH_SERVER_URL, constants.USER_PROFILE_SERVER_URL], options); 58 | this.userProfilesServerUrl = config.getUserProfile() || options[constants.USER_PROFILE_SERVER_URL] || 59 | vcapServiceCredentials[constants.USER_PROFILE_SERVER_URL]; 60 | this.oauthServerUrl = config.getOAuthServerUrl() || options[constants.OAUTH_SERVER_URL] || vcapServiceCredentials[constants.OAUTH_SERVER_URL]; 61 | } catch (e) { 62 | logger.error("Failed to initialize user-manager."); 63 | logger.error("Ensure your node.js app is either bound to an App ID service instance or pass required parameters to the constructor "); 64 | logger.error(e); 65 | if (options.throwIfFail) { 66 | throw e; 67 | } 68 | } 69 | logger.info(constants.USER_PROFILE_SERVER_URL, this.userProfilesServerUrl); 70 | logger.info(constants.OAUTH_SERVER_URL, this.oauthServerUrl); 71 | }; 72 | 73 | UserProfileManager.prototype.setAttribute = function (accessToken, attributeName, attributeValue) { 74 | const deferred = Q.defer(); 75 | if (!_.isString(accessToken) || !_.isString(attributeName) || !_.isString(attributeValue)) { 76 | logger.error("setAttribute invalid invocation parameters"); 77 | return Q.reject(); 78 | } 79 | var profilesUrl = this.userProfilesServerUrl + ATTRIBUTES_ENDPOINT + "/" + attributeName; 80 | handleRequest(accessToken, attributeValue, "PUT", profilesUrl, "setAttribute", deferred); 81 | return deferred.promise; 82 | }; 83 | 84 | UserProfileManager.prototype.getAttribute = function (accessToken, attributeName) { 85 | const deferred = Q.defer(); 86 | if (!_.isString(accessToken) || !_.isString(attributeName)) { 87 | logger.error("getAttribute invalid invocation parameters"); 88 | return Q.reject(); 89 | } 90 | logger.debug("Getting attribute ", attributeName); 91 | var profilesUrl = this.userProfilesServerUrl + ATTRIBUTES_ENDPOINT + "/" + attributeName; 92 | handleRequest(accessToken, null, "GET", profilesUrl, "getAttribute", deferred); 93 | return deferred.promise; 94 | 95 | }; 96 | 97 | UserProfileManager.prototype.deleteAttribute = function (accessToken, attributeName) { 98 | const deferred = Q.defer(); 99 | if (!_.isString(accessToken) || !_.isString(attributeName)) { 100 | logger.error("deleteAttribute invalid invocation parameters"); 101 | return Q.reject(); 102 | } 103 | logger.debug("Deleting attribute", attributeName); 104 | var profilesUrl = this.userProfilesServerUrl + ATTRIBUTES_ENDPOINT + "/" + attributeName; 105 | handleRequest(accessToken, null, "DELETE", profilesUrl, "deleteAttribute", deferred); 106 | return deferred.promise; 107 | 108 | }; 109 | 110 | 111 | UserProfileManager.prototype.getAllAttributes = function (accessToken) { 112 | const deferred = Q.defer(); 113 | if (!_.isString(accessToken)) { 114 | logger.error("getAllAttributes invalid invocation parameters"); 115 | return Q.reject(); 116 | } 117 | logger.debug("Getting all attributes"); 118 | var profilesUrl = this.userProfilesServerUrl + ATTRIBUTES_ENDPOINT; 119 | handleRequest(accessToken, null, "GET", profilesUrl, "getAllAttributes", deferred); 120 | return deferred.promise; 121 | }; 122 | 123 | /** 124 | * Retrieves user info using the provided accessToken 125 | * 126 | * @param {string} accessToken - the accessToken string used for authorization 127 | * @param {string|undefined} identityToken - an optional identity token. If provided, will be used to validate UserInfo response 128 | * @returns {Promise} - the user info in json format 129 | */ 130 | UserProfileManager.prototype.getUserInfo = function (accessToken, identityToken) { 131 | const deferred = Q.defer(); 132 | const internalDeferred = Q.defer(); 133 | 134 | if (!_.isString(accessToken)) { 135 | logger.error("getUserinfo invalid invocation parameter"); 136 | return Q.reject(new Error("Invalid invocation parameter type. Access token must be a string.")); 137 | } 138 | 139 | logger.debug("Getting userinfo"); 140 | 141 | const serverUrl = this.oauthServerUrl + USERINFO_ENDPOINT; 142 | handleRequest(accessToken, null, "GET", serverUrl, "getUserInfo", internalDeferred); // eslint-disable-line no-use-before-define 143 | 144 | internalDeferred.promise 145 | .then(function (userInfo) { // eslint-disable-line complexity 146 | try { 147 | if (!(userInfo && userInfo.sub)) { 148 | throw new Error("Invalid user info response"); 149 | } 150 | 151 | // If identity token is provided we must validate the subject matches the UserInfo response subject 152 | if (identityToken) { 153 | 154 | let payload = Utils.decode(identityToken); 155 | 156 | if (!payload) { 157 | throw new Error("Invalid Identity Token"); 158 | } 159 | 160 | if (payload.sub && userInfo.sub !== payload.sub) { 161 | throw new Error("Possible token substitution attack. Rejecting request userInfoResponse.sub != identityToken.sub"); 162 | } 163 | } 164 | 165 | deferred.resolve(userInfo); 166 | 167 | } catch (e) { 168 | logger.error("getUserInfo failed " + e.message); 169 | return deferred.reject(e); 170 | } 171 | }) 172 | .catch(deferred.reject); 173 | 174 | return deferred.promise; 175 | }; 176 | 177 | function handleRequest(accessToken, reqBody, method, url, action, deferred) { 178 | 179 | request({ 180 | url: url, 181 | method: method, 182 | body: reqBody, 183 | headers: { 184 | "Authorization": "Bearer " + accessToken 185 | } 186 | }, function (err, response, body) { 187 | if (err) { 188 | logger.error(err); 189 | return deferred.reject(new Error("Failed to " + action)); 190 | } else if (response.statusCode === 401 || response.statusCode === 403) { 191 | return deferred.reject(new UnauthorizedException()); 192 | } else if (response.statusCode === 404) { 193 | return deferred.reject(new Error("Not found")); 194 | } else if (response.statusCode >= 200 && response.statusCode < 300) { 195 | return deferred.resolve(body ? body : null); 196 | } else { 197 | logger.error(err, response.headers); 198 | return deferred.reject(new Error("Unexpected error")); 199 | } 200 | }); 201 | } 202 | 203 | module.exports = new UserProfileManager(); -------------------------------------------------------------------------------- /lib/utils/common-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const FormData = require('form-data'); 15 | 16 | module.exports = (function () { 17 | 18 | function objectKeysToLowerCase(input) { 19 | if (typeof input !== 'object') return input; 20 | if (Array.isArray(input)) return input.map(objectKeysToLowerCase); 21 | return Object.keys(input).reduce(function (newObj, key) { 22 | let val = input[key]; 23 | let newVal = (typeof val === 'object') ? objectKeysToLowerCase(val) : val; 24 | newObj[key.toLowerCase()] = newVal; 25 | return newObj; 26 | }, {}); 27 | }; 28 | 29 | // Null safe operator 30 | function optionalChaining(fn, defaultVal) { 31 | try { 32 | return fn(); 33 | } catch (e) { 34 | return defaultVal; 35 | } 36 | } 37 | 38 | function jsonToURLencodedForm(srcjson) { 39 | if (typeof srcjson !== "object") { 40 | return srcjson; 41 | } 42 | 43 | let u = encodeURIComponent; 44 | let urljson = ""; 45 | let keys = Object.keys(srcjson); 46 | for (var i = 0; i < keys.length; i++) { 47 | urljson += u(keys[i]) + "=" + u(srcjson[keys[i]]); 48 | if (i < (keys.length - 1)) urljson += "&"; 49 | } 50 | return urljson; 51 | } 52 | 53 | function parseJSON(jsonStr) { 54 | try { 55 | var json = JSON.parse(jsonStr); 56 | return json; 57 | } catch (e) { 58 | return jsonStr; 59 | } 60 | } 61 | 62 | 63 | const createFormData = object => Object.keys(object).reduce((formData, key) => { 64 | formData.append(key, object[key]); 65 | return formData; 66 | }, new FormData()); 67 | 68 | 69 | const parseFormData = (form) => form._streams.reduce((result, line) => { 70 | if (typeof line === 'string') { 71 | let matches = line.match(/name="(.+)"/); 72 | let key; 73 | 74 | 75 | if (typeof line.match(/name="(.+)"/) !== 'undefined') { 76 | matches = line.match(/name="(.+)"/); 77 | 78 | if (matches && matches.length > 0) { 79 | key = matches[1]; 80 | } 81 | } 82 | 83 | 84 | if (key) { 85 | result._currentKey = key; 86 | } else if (line !== '\\r\\n') { 87 | result[result._currentKey] = line; 88 | delete result._currentKey; 89 | } 90 | } 91 | 92 | return result; 93 | }, {}); 94 | 95 | return { 96 | objectKeysToLowerCase, 97 | optionalChaining, 98 | jsonToURLencodedForm, 99 | parseJSON, 100 | createFormData, 101 | parseFormData 102 | }; 103 | 104 | }()); -------------------------------------------------------------------------------- /lib/utils/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | VCAP_SERVICES: "VCAP_SERVICES", 3 | VCAP_SERVICES_CREDENTIALS: "credentials", 4 | VCAP_SERVICES_SERVICE_NAME1: "AdvancedMobileAccess", 5 | VCAP_SERVICES_SERVICE_NAME2: "AppID", 6 | VCAP_APPLICATION: "VCAP_APPLICATION", 7 | TENANT_ID: "tenantId", 8 | CLIENT_ID: "clientId", 9 | SECRET: "secret", 10 | OAUTH_SERVER_URL: "oauthServerUrl", 11 | REDIRECT_URI: "redirectUri", 12 | PREFERRED_LOCALE: "preferredLocale", 13 | ISS: "iss", 14 | AUD: "aud", 15 | EXP: "exp", 16 | TENANT: "tenant", 17 | VERSION: "ver", 18 | CUSTOM_IDENTITY_GRANT_TYPE: "urn:ietf:params:oauth:grant-type:jwt-bearer", 19 | USER_PROFILE_SERVER_URL : "profilesUrl", 20 | APP_TO_APP_GRANT_TYPE: "client_credentials", 21 | TOKEN_PATH: "/token", 22 | APPID_SERVICE_ENDPOINT: "appidServiceEndpoint", 23 | APPID_SERVICE_VERSION: "version", 24 | APPID_TENANT_ID: "tenantId", 25 | APPID_ISSUER: "issuer" 26 | }; 27 | -------------------------------------------------------------------------------- /lib/utils/public-key-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const log4js = require("log4js"); 15 | 16 | const logger = log4js.getLogger("appid-public-key-util"); 17 | const Q = require("q"); 18 | const request = require('../utils/request-util'); 19 | const pemFromModExp = require("rsa-pem-from-mod-exp"); 20 | 21 | const TIMEOUT = 15 * 1000; 22 | 23 | var events = require("events"); 24 | var eventEmitter = new events.EventEmitter(); 25 | 26 | module.exports = (function () { 27 | logger.debug("Initializing"); 28 | let publicKeysJson = {}; 29 | let isUpdateRequestPending = {}; 30 | 31 | 32 | function getPublicKeyByKid(tokenKid, localPublicKeysEndpoint) { 33 | logger.debug("GetPublicKeyByKID endpoint", localPublicKeysEndpoint); 34 | if (publicKeysJson[localPublicKeysEndpoint]) { 35 | var singlePublicKeysJson = publicKeysJson[localPublicKeysEndpoint]; 36 | for (var i = 0; i < singlePublicKeysJson.length; i++) { 37 | if (singlePublicKeysJson[i].kid === tokenKid) { 38 | return singlePublicKeysJson[i]; 39 | } 40 | } 41 | } 42 | return null; 43 | } 44 | 45 | function _updatePublicKeys(publicKeysUrl) { 46 | let deferred = Q.defer(); 47 | logger.debug("Getting public key from", publicKeysUrl); 48 | request({ 49 | method: "GET", 50 | url: publicKeysUrl, 51 | json: true, 52 | timeout: TIMEOUT, 53 | headers: { 54 | "x-filter-type": "nodejs" 55 | } 56 | }, function (error, response, body) { 57 | 58 | if (error || response.statusCode !== 200) { 59 | if (typeof publicKeysJson[publicKeysUrl] === 'undefined') { 60 | deferred.reject("Failed to retrieve public keys. All requests to protected endpoints will be rejected."); 61 | } else { 62 | deferred.reject("Failed to update public keys."); 63 | } 64 | } else { 65 | deferred.resolve(body.keys); 66 | } 67 | }); 68 | return deferred.promise; 69 | } 70 | 71 | function getPublicKeyPemByKid(tokenKid, serverUrl) { 72 | let deferred = Q.defer(); 73 | 74 | let localPublicKeysEndpoint = serverUrl + '/publickeys'; 75 | let emitterEventName = "publicKeysUpdated" + localPublicKeysEndpoint; //needs to be unique 76 | if (tokenKid) { 77 | let publicKey = getPublicKeyByKid(tokenKid, localPublicKeysEndpoint); 78 | if (!publicKey) { // if not found, refresh public keys array 79 | eventEmitter.once(emitterEventName, function (error) { 80 | let publicKeys = getPublicKeyByKid(tokenKid, localPublicKeysEndpoint); 81 | if (publicKeys) { 82 | deferred.resolve(pemFromModExp(publicKeys.n, publicKeys.e)); 83 | } else { 84 | if (!error) { 85 | deferred.reject("Public key not found for given token kid"); 86 | } else { 87 | deferred.reject(error); 88 | } 89 | } 90 | }); 91 | if (!isUpdateRequestPending[localPublicKeysEndpoint]) { 92 | isUpdateRequestPending[localPublicKeysEndpoint] = true; 93 | _updatePublicKeys(localPublicKeysEndpoint).then(function (keys) { 94 | publicKeysJson[localPublicKeysEndpoint] = keys; 95 | logger.info("Public keys updated"); 96 | isUpdateRequestPending[localPublicKeysEndpoint] = false; 97 | eventEmitter.emit(emitterEventName, null, localPublicKeysEndpoint); 98 | }).catch(function (e) { 99 | let error = "updatePublicKeys error: " + e; 100 | logger.error(error); 101 | isUpdateRequestPending[localPublicKeysEndpoint] = false; 102 | eventEmitter.emit(emitterEventName, error, localPublicKeysEndpoint); 103 | }); 104 | } 105 | } else { 106 | deferred.resolve(pemFromModExp(publicKey.n, publicKey.e)); 107 | } 108 | } else { 109 | deferred.reject("Passed token does not have kid value."); 110 | } 111 | return deferred.promise; 112 | } 113 | 114 | return { 115 | getPublicKeyPemByKid 116 | }; 117 | }()); -------------------------------------------------------------------------------- /lib/utils/request-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | 15 | /* 16 | This function will wrap the GotJS promise call in a function Callback 17 | So it can be a drop-in replacement for the deprecated request library 18 | */ 19 | 20 | const got = require('got'); 21 | const { 22 | objectKeysToLowerCase, 23 | optionalChaining, 24 | parseJSON, 25 | jsonToURLencodedForm, 26 | createFormData 27 | } = require('./common-util'); 28 | 29 | module.exports = (function () { 30 | 31 | // Drop-in replacement for the deprecated request library 32 | function request(options, callback) { 33 | let error = null; 34 | let response = null; 35 | let body = null; 36 | 37 | (async () => { 38 | try { 39 | if (options.method !== 'GET') { 40 | options.headers = options.headers || {}; 41 | 42 | options.headers = objectKeysToLowerCase(options.headers); 43 | options.headers["content-type"] = options.headers["content-type"] || 'application/json'; 44 | } 45 | 46 | // GotJS doesn't allow Request Body with the GET OR DELETE Method by default. 47 | if(options.method === 'GET' || 'DELETE') { 48 | if(options.body === null) { 49 | delete options.body; 50 | } 51 | else { 52 | options.allowGetBody = true; 53 | } 54 | } 55 | 56 | if (options.qs) { 57 | options.searchParams = options.qs; 58 | delete options.qs; 59 | } 60 | 61 | if (options.form) { 62 | if (options.headers["content-type"] === 'application/x-www-form-urlencoded') { 63 | options.body = jsonToURLencodedForm(options.form); 64 | } else { 65 | options.body = JSON.stringify(options.form); 66 | } 67 | delete options.form; 68 | } 69 | 70 | if (options.auth) { 71 | const authBearer = optionalChaining(() => options.auth.bearer); 72 | if (authBearer) { 73 | options.headers["Authorization"] = `Bearer ${authBearer}`; 74 | delete options.auth; 75 | } 76 | 77 | const authUsername = optionalChaining(() => options.auth.username); 78 | const authPassword = optionalChaining(() => options.auth.password); 79 | if (authUsername && authPassword) { 80 | options.headers["Authorization"] = "Basic " + Buffer.from(`${authUsername}:${authPassword}`).toString("base64"); 81 | delete options.auth; 82 | } 83 | } 84 | 85 | if (options.formData) { 86 | options.body = createFormData(options.formData); 87 | 88 | // Remove the default content-type 89 | if (options.headers && options.headers["content-type"] === 'application/json') { 90 | delete options.headers["content-type"]; 91 | } 92 | 93 | delete options.formData; 94 | } 95 | 96 | // receive a JSON body 97 | // This is necessary for the ServerSDK only as we are expecting JSON response in all the calls 98 | if (!options.responseType) { 99 | options.responseType = 'json'; 100 | } 101 | 102 | if (options.json) { 103 | // Handle json param same as Request library used to handle, so If json is true, then body must be a JSON-serializable object. 104 | if(typeof options.json == "boolean") { 105 | options.responseType = 'json'; 106 | } 107 | // Handle json Request as Request library used to do, so If json is true, then body must be a JSON-serializable object. 108 | else { 109 | options.body = JSON.stringify(options.json); 110 | } 111 | delete options.json; 112 | } 113 | 114 | // Handle json param same as Request library used to handle, so If json is false, then body must be Not a JSON-serializable object. 115 | if (typeof options.json == "boolean" && !options.json) { 116 | delete options.json; 117 | delete options.responseType; 118 | } 119 | 120 | // requests that encounter an error status code will be resolved with the response instead of throwing 121 | options.throwHttpErrors = false; 122 | 123 | // remove url from options 124 | const url = options.url; 125 | delete options.url; 126 | 127 | // Main Request Call 128 | response = await got(url, options); 129 | body = parseJSON(response.body); 130 | 131 | if (response.error) { 132 | error = response.error; 133 | } 134 | callback(error, response, body); 135 | } catch (err) { 136 | error = err; 137 | if (err.response && err.response.statusCode) { 138 | statusCode = err.response.statusCode; 139 | } 140 | 141 | // If body is empty, then return the response details of the error object 142 | if(!body && err.response) { 143 | response = err.response; 144 | body = err.response.body; 145 | response.statusCode = statusCode; 146 | } 147 | 148 | callback(error, response, body); 149 | } 150 | })(); 151 | } 152 | 153 | return request; 154 | }()); -------------------------------------------------------------------------------- /lib/utils/service-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const log4js = require("log4js"); 15 | 16 | let logger; 17 | const constants = require("./constants"); 18 | 19 | 20 | /** 21 | /** serviceConfig 22 | @typedef {Object} ServiceConfig 23 | * @property {function():string} getConfig - returns the entire config 24 | * @property {function():string} getTenantId - returns the tenantId 25 | * @property {function():string} getSecret - returns the secret; 26 | * @property {function():string} getOAuthServerUrl - returns the server url 27 | * @property {function():string} getRedirectUri - returns the redirect uri 28 | * @property {function():string} getPreferredLocale - returns the preferred locale 29 | * @property {function():string} getIssuer - returns the issuer 30 | * @property {function():string} getUserProfile - returns the user profiles endpoint 31 | * @property {function(string)} setIssuer - sets the value of the issuer 32 | */ 33 | 34 | /** 35 | * loads a config 36 | * @param {string}configName the name of the config 37 | * @param {string[]}requiredParams an array for the required params 38 | * @param {object} options the options for the configuration 39 | * @return {ServiceConfig} the configration 40 | */ 41 | function loadConfig(configName, requiredParams, options={}) { 42 | logger = log4js.getLogger(`appid-${configName}-config`); 43 | logger.debug(`Initializing ${configName} config`); 44 | 45 | const vcapServices = JSON.parse(process.env[constants.VCAP_SERVICES] || "{}"); 46 | let vcapServiceCredentials = {}; 47 | const removeTrailingSlash = (url) => url && url.replace && url.replace(/\/$/, ""); 48 | 49 | 50 | for (let propName in vcapServices) { 51 | // Checks if string starts with the service name 52 | if (propName.indexOf(constants.VCAP_SERVICES_SERVICE_NAME1) === 0 || propName.indexOf(constants.VCAP_SERVICES_SERVICE_NAME2) === 0) { 53 | vcapServiceCredentials = vcapServices[propName][0][constants.VCAP_SERVICES_CREDENTIALS]; 54 | break; 55 | } 56 | } 57 | const findParam = (param) => options[param] || vcapServiceCredentials[param] || process.env[param]; 58 | const serviceEndpoint = findParam(constants.APPID_SERVICE_ENDPOINT); 59 | const serviceVersion = findParam(constants.APPID_SERVICE_VERSION); 60 | const serviceTenantID = findParam(constants.APPID_TENANT_ID); 61 | let serviceConfig = {}; 62 | if (serviceEndpoint) { 63 | if (!serviceVersion || !Number.isInteger(Number.parseInt(serviceVersion))) { 64 | throw new Error("Failed to initialize APIStrategy. Missing version parameter, should be an integer."); 65 | } else if (!serviceTenantID) { 66 | throw new Error("Failed to initialize APIStrategy. Missing tenantId parameter"); 67 | } else { 68 | serviceConfig[constants.OAUTH_SERVER_URL] = `${removeTrailingSlash(serviceEndpoint)}/oauth/v${serviceVersion}/${ 69 | serviceTenantID}`; 70 | serviceConfig[constants.USER_PROFILE_SERVER_URL] = removeTrailingSlash(serviceEndpoint); 71 | } 72 | 73 | } else { 74 | serviceConfig[constants.OAUTH_SERVER_URL] = findParam(constants.OAUTH_SERVER_URL); 75 | serviceConfig[constants.USER_PROFILE_SERVER_URL] = findParam(constants.USER_PROFILE_SERVER_URL); 76 | if (findParam('oAuthServerUrl')) { 77 | serviceConfig[constants.OAUTH_SERVER_URL] = findParam('oAuthServerUrl'); 78 | } 79 | } 80 | serviceConfig[constants.TENANT_ID] = serviceTenantID; 81 | serviceConfig[constants.CLIENT_ID] = findParam(constants.CLIENT_ID); 82 | serviceConfig[constants.SECRET] = findParam(constants.SECRET); 83 | 84 | serviceConfig[constants.REDIRECT_URI] = options[constants.REDIRECT_URI] || process.env[constants.REDIRECT_URI]; 85 | serviceConfig[constants.PREFERRED_LOCALE] = options[constants.PREFERRED_LOCALE]; 86 | serviceConfig[constants.APPID_ISSUER] = options[constants.APPID_ISSUER] || process.env[constants.APPID_ISSUER]; 87 | if (!serviceConfig[constants.REDIRECT_URI]) { 88 | let vcapApplication = process.env[constants.VCAP_APPLICATION]; 89 | if (vcapApplication) { 90 | vcapApplication = JSON.parse(vcapApplication); 91 | serviceConfig[constants.REDIRECT_URI] = "https://" + vcapApplication["application_uris"][0] + "/ibm/bluemix/appid/callback"; 92 | } 93 | } 94 | 95 | requiredParams.map((param) => { 96 | if (!serviceConfig[param]) { 97 | throw Error(`Failed to initialize ${configName}. Missing ${param} parameter.`); 98 | } else if (param === constants.SECRET) { 99 | logger.info(param, '[CANNOT LOG SECRET]'); 100 | } else { 101 | logger.info(param, serviceConfig[param]); 102 | } 103 | }); 104 | 105 | if (serviceConfig[constants.PREFERRED_LOCALE]) { 106 | logger.info(constants.PREFERRED_LOCALE, serviceConfig[constants.PREFERRED_LOCALE]); 107 | } 108 | 109 | //getters 110 | const getConfig = () => serviceConfig; 111 | const getTenantId = () => serviceConfig[constants.TENANT_ID]; 112 | const getClientId = () => serviceConfig[constants.CLIENT_ID]; 113 | const getSecret = () => serviceConfig[constants.SECRET]; 114 | const getOAuthServerUrl = () => removeTrailingSlash(serviceConfig[constants.OAUTH_SERVER_URL]); 115 | const getRedirectUri = () => serviceConfig[constants.REDIRECT_URI]; 116 | const getPreferredLocale = () => serviceConfig[constants.PREFERRED_LOCALE]; 117 | const getIssuer = () => serviceConfig[constants.APPID_ISSUER]; 118 | const getUserProfile = () => serviceConfig[constants.USER_PROFILE_SERVER_URL]; 119 | //setters 120 | const setIssuer = (value) => serviceConfig[constants.APPID_ISSUER] = value; 121 | 122 | 123 | return { 124 | getConfig, 125 | getTenantId, 126 | getClientId, 127 | getSecret, 128 | getOAuthServerUrl, 129 | getRedirectUri, 130 | getPreferredLocale, 131 | getIssuer, 132 | getUserProfile, 133 | setIssuer 134 | }; 135 | } 136 | 137 | module.exports = {loadConfig}; 138 | -------------------------------------------------------------------------------- /lib/utils/token-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const request = require('./request-util'); 15 | const crypto = require("crypto"); 16 | const jwt = require("jsonwebtoken"); 17 | const Q = require("q"); 18 | const log4js = require("log4js"); 19 | const publicKeyUtil = require("./public-key-util"); 20 | const constants = require("./constants"); 21 | 22 | const logger = log4js.getLogger("appid-token-util"); 23 | const TIMEOUT = 15 * 1000; //15 seconds 24 | const APPID_ALLOW_EXPIRED_TOKENS = "APPID_ALLOW_EXPIRED_TOKENS"; 25 | const alg = "RS256"; 26 | const bytes = 20; 27 | /** 28 | * 29 | * @type {{decodeAndValidate, decode, validateIssAndAud, getRandomNumber}} 30 | */ 31 | module.exports = (function () { 32 | logger.debug("Initializing"); 33 | 34 | function decode(tokenString, getHeader) { 35 | return jwt.decode(tokenString, { 36 | complete: getHeader 37 | }); 38 | } 39 | 40 | function decodeAndValidate(tokenString, serverUrl) { 41 | var deferred = Q.defer(); 42 | 43 | var allowExpiredTokens = process.env.APPID_ALLOW_EXPIRED_TOKENS || false; 44 | if (allowExpiredTokens) { 45 | logger.warn(APPID_ALLOW_EXPIRED_TOKENS, "is enabled. Make sure to disable it in production environments."); 46 | } 47 | var token = decode(tokenString, true); 48 | var tokenHeader = token ? token.header : null; 49 | 50 | if (!tokenHeader) { 51 | deferred.reject("JWT error, can not decode token"); 52 | return deferred.promise; 53 | } 54 | 55 | publicKeyUtil.getPublicKeyPemByKid(tokenHeader.kid, serverUrl).then(function (publicKeyPem) { 56 | try { 57 | var decodedToken = jwt.verify(tokenString, publicKeyPem, { 58 | algorithms: alg, 59 | ignoreExpiration: allowExpiredTokens, 60 | json: true 61 | }); 62 | decodedToken.ver = tokenHeader.ver; 63 | deferred.resolve(decodedToken); 64 | } catch (err) { 65 | logger.debug("JWT error ::", err.message); 66 | deferred.reject(err); 67 | } 68 | }).catch(function (err) { 69 | deferred.reject(err); 70 | }); 71 | 72 | return deferred.promise; 73 | } 74 | 75 | 76 | /** getOauthServer 77 | @name getOAuthServerUrl 78 | @function 79 | @return {String} appid's server endpoint 80 | */ 81 | /** 82 | * @async 83 | * return the issuer from the wellknown endpoint 84 | * @param {ServiceConfig} serviceConfig the server configuration 85 | * @param {getOAuthServerUrl} serviceConfig.getOAuthServerUrl returns the server url 86 | * @return {Promise} the issuer as a string or an error if the issuer wasn't found 87 | */ 88 | function updateWellKnownIssuer(serviceConfig) { 89 | const wellKnownEndpoint = `${serviceConfig.getOAuthServerUrl()}/.well-known/openid-configuration`; 90 | 91 | //get /oauth/v3/{tenantId}/.well-known/openid-configuration 92 | 93 | 94 | return new Promise(function (resolve, reject) { 95 | request({ 96 | method: "GET", 97 | url: wellKnownEndpoint, 98 | json: true, 99 | timeout: TIMEOUT 100 | }, function (error, response, body) { 101 | if (error || response.statusCode !== 200) { 102 | const errmsg = "Failed to get issuer from well known endpoint"; 103 | logger.error(errmsg, error); 104 | reject(errmsg); 105 | } else if (body.issuer) { 106 | logger.debug("Got ISSUER from well known endpoint: " + body.issuer); 107 | serviceConfig.setIssuer(body.issuer); 108 | resolve(body.issuer); 109 | } else { 110 | const errmsg = "Failed to get issuer from well known endpoint, missing issuer field."; 111 | logger.error(errmsg); 112 | reject(errmsg); 113 | } 114 | }); 115 | }); 116 | } 117 | 118 | const issuerConversion = (issuer, tenantId, version) => { 119 | if (issuer.includes('bluemix.net')) { 120 | const regionEnd = issuer.indexOf('.bluemix.net'); 121 | const regionStart = issuer.slice(0, regionEnd).lastIndexOf('.') + 1; 122 | let region = issuer.slice(regionStart, regionEnd); 123 | region = region.replace('ng', 'us-south'); // change only bluemix region w/different ibmcloud version 124 | 125 | const test = issuer.includes('stage1') ? 'test.' : ''; 126 | return `https://${region}.appid.${test}cloud.ibm.com/oauth/v${version}/${tenantId}`; 127 | } 128 | return issuer; 129 | }; 130 | 131 | function getVersion(token) { 132 | logger.info(`token ${constants.VERSION}: ${token[constants.VERSION]}`); 133 | const version = token[constants.VERSION]; 134 | if (version === undefined) { 135 | logger.info(`token version is undefined, will treat token as v3`); 136 | return 3; 137 | } else if (Number.isInteger(version)) { 138 | logger.info(`token version is valid`); 139 | return version; 140 | } 141 | throw new Error(`Invalid token version: ${token[constants.VERSION]}`); 142 | } 143 | 144 | function checkAud(token, clientId, version) { 145 | logger.info(`token ${constants.AUD}: ${token[constants.AUD]}`); 146 | if (version && version >= 4) { 147 | if (!Array.isArray(token[constants.AUD])) { 148 | logger.error(`Expected post-v4 token ${constants.AUD} to be an array`); 149 | return false; 150 | } 151 | if (!token[constants.AUD].includes(clientId)) { 152 | logger.error(`Expected post-v4 token ${constants.AUD} to include ${clientId} in its array`); 153 | return false; 154 | } 155 | } else if (token[constants.AUD] !== clientId) { 156 | logger.error(`Expected pre-v4 token ${constants.AUD} to be ${clientId}`); 157 | return false; 158 | } 159 | return true; 160 | } 161 | 162 | function checkIss(token, issuer, version) { 163 | const tokenIssuer = token[constants.ISS]; 164 | let configIssuer = issuer; 165 | logger.info(`token ${constants.ISS}: ${tokenIssuer}`); 166 | 167 | if (version && version >= 4) { 168 | const tenantId = token[constants.TENANT]; 169 | configIssuer = issuerConversion(issuer, tenantId, version); 170 | } 171 | 172 | if (tokenIssuer !== configIssuer) { 173 | logger.error(`Expected token ${constants.ISS} to be ${configIssuer}`); 174 | return false; 175 | } 176 | return true; 177 | } 178 | 179 | /** 180 | * Checks if the "exp" field in the provided token is expired. 181 | * @param tokenPayload: decoded access token Json. 182 | * @returns {boolean}: true if expired, false if not expired. 183 | */ 184 | function isTokenExpired(tokenPayload) { 185 | if (!tokenPayload || !tokenPayload[constants.EXP]) { 186 | logger.error(`invalid access token payload: `, tokenPayload); 187 | throw new Error('invalid token payload.'); 188 | } 189 | if (Date.now() >= tokenPayload[constants.EXP] * 1000) { 190 | logger.info('token expired.', tokenPayload); 191 | return true; 192 | } 193 | return false; 194 | } 195 | 196 | /** 197 | * @async 198 | * validates the issuer and audience of a token 199 | * @param {object} token the token that should ve validated 200 | * @param {ServiceConfig} serviceConfig the server configuration 201 | * @param {getOAuthServerUrl} serviceConfig.getOAuthServerUrl returns the server url 202 | * @return {Promise} true if the issuer is valid, reject with an error if it doesn't 203 | */ 204 | function validateIssAndAud(token, serviceConfig) { 205 | return Promise.resolve() 206 | .then(() => { 207 | 208 | logger.debug("Validating Iss and Aud claims"); 209 | 210 | const version = getVersion(token); 211 | 212 | const clientId = serviceConfig.getClientId(); 213 | if (!checkAud(token, clientId, version)) { 214 | throw new Error("Failed to validate audience claim."); 215 | } 216 | 217 | const issuer = serviceConfig.getIssuer(); 218 | if (issuer) { 219 | if (checkIss(token, issuer, version)) { 220 | logger.debug("Successfully validated Iss and Aud claims with user defined issuer"); 221 | return true; 222 | } else { 223 | throw new Error("User defined issuer does not match token issuer."); 224 | } 225 | } 226 | 227 | // If issuer was not yet retrieved from AppId /oauth/{version}/{tenantId}/.well-known/openid-configuration 228 | return updateWellKnownIssuer(serviceConfig).then(retrievedIssuer => { 229 | if (checkIss(token, retrievedIssuer, version)) { 230 | logger.debug("Successfully validated Iss and Aud claims with a well-known issuer"); 231 | return true; 232 | } else { 233 | throw new Error("Invalid issuer from well-known endpoint"); 234 | } 235 | }); 236 | }); 237 | } 238 | 239 | function getRandomNumber() { 240 | return crypto.randomBytes(bytes).toString('base64').replace(/\//g, '_').replace(/\+/g, '-'); 241 | } 242 | 243 | return { 244 | decodeAndValidate, 245 | decode, 246 | validateIssAndAud, 247 | getRandomNumber, 248 | isTokenExpired 249 | }; 250 | }()); -------------------------------------------------------------------------------- /log4js.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders": { 3 | "out": { 4 | "type": "console" 5 | } 6 | }, 7 | "categories": { 8 | "default": { 9 | "appenders": [ 10 | "out" 11 | ], 12 | "level": "info" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibmcloud-appid", 3 | "version": "7.0.0", 4 | "description": "Node.js SDK for the IBM Cloud App ID service", 5 | "main": "lib/appid-sdk.js", 6 | "scripts": { 7 | "apiapp": "node samples/api-app-sample-server.js", 8 | "webapp": "node samples/web-app-sample-server.js", 9 | "commit": "git-cz", 10 | "test": "./node_modules/.bin/_mocha", 11 | "coverage": "./node_modules/.bin/nyc ./node_modules/.bin/_mocha --timeout 30000 --exit", 12 | "semantic-release": "semantic-release", 13 | "acp": "git add . && npm run commit" 14 | }, 15 | "types": "./lib/appid-sdk.d.ts", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/ibm-cloud-security/appid-serversdk-nodejs.git" 19 | }, 20 | "author": "Anton Aleksandrov", 21 | "license": "Apache-2.0", 22 | "devDependencies": { 23 | "base64url": "^3.0.1", 24 | "chai": "^4.2.0", 25 | "chai-as-promised": "^7.1.1", 26 | "coveralls": "^3.1.1", 27 | "cz-conventional-changelog": "^3.3.0", 28 | "eslint": "^5.16.0", 29 | "eslint-config-airbnb-base": "^13.1.0", 30 | "eslint-plugin-import": "^2.22.0", 31 | "eslint-plugin-node": "^8.0.0", 32 | "express": "^4.17.3", 33 | "express-session": "^1.17.3", 34 | "mocha": "^5.2.0", 35 | "nyc": "^15.0.0", 36 | "passport": "0.6.0", 37 | "proxyquire": "^2.1.3", 38 | "rewire": "^4.0.1", 39 | "semantic-release": "^18.0.0", 40 | "sinon-chai": "^3.5.0", 41 | "sinon": "^9.2.4" 42 | }, 43 | "dependencies": { 44 | "accept-language": "^3.0.18", 45 | "body-parser": "^1.19.0", 46 | "connect-flash": "^0.1.1", 47 | "cookie-parser": "^1.4.6", 48 | "ejs": "^3.1.8", 49 | "form-data": "^2.3.3", 50 | "got": "^11.8.5", 51 | "helmet": "^3.23.3", 52 | "jsonwebtoken": "^9.0.0", 53 | "log4js": "^6.4.1", 54 | "tsd": "^0.20.0", 55 | "q": "^1.5.1", 56 | "rsa-pem-from-mod-exp": "^0.8.4", 57 | "underscore": "^1.10.2" 58 | }, 59 | "config": { 60 | "commitizen": { 61 | "path": "./node_modules/cz-conventional-changelog" 62 | } 63 | }, 64 | "engines" : { 65 | "node" : ">=12.0.0" 66 | } 67 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello 6 | 7 | 8 | Hello 9 | 10 | -------------------------------------------------------------------------------- /samples/api-app-sample-server.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const express = require("express"); 15 | const log4js = require("log4js"); 16 | const passport = require("passport"); 17 | const APIStrategy = require("./../lib/appid-sdk").APIStrategy; 18 | const helmet = require("helmet"); 19 | 20 | const app = express(); 21 | const logger = log4js.getLogger("testApp"); 22 | 23 | app.use(helmet()); 24 | app.use(passport.initialize()); 25 | 26 | passport.use(new APIStrategy({ 27 | oauthServerUrl: "{oauth-server-url}" 28 | })); 29 | 30 | app.get("/api/protected", 31 | passport.authenticate(APIStrategy.STRATEGY_NAME, { 32 | session: false 33 | }), 34 | function(req, res) { 35 | // Get full appIdAuthorizationContext from request object 36 | var appIdAuthContext = req.appIdAuthorizationContext; 37 | 38 | appIdAuthContext.accessToken; // Raw access_token 39 | appIdAuthContext.accessTokenPayload; // Decoded access_token JSON 40 | appIdAuthContext.identityToken; // Raw identity_token 41 | appIdAuthContext.identityTokenPayload; // Decoded identity_token JSON 42 | 43 | // Or use user object provided by passport.js 44 | var username = req.user ? req.user.name : "Anonymous"; 45 | res.send("Hello from protected resource " + username); 46 | } 47 | ); 48 | 49 | var port = process.env.PORT || 1234; 50 | 51 | app.listen(port, function(){ 52 | logger.info("Send GET request to http://localhost:" + port + "/api/protected"); 53 | }); 54 | 55 | -------------------------------------------------------------------------------- /samples/browser-app-sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebAppSample 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

This is a browser-app-sample for appid-serversdk-nodejs

14 |

It demonstrates how to use appid-serversdk-nodejs in a browser web all (single-page-app).

15 | 16 | Coming soon.... 17 | 38 | 39 | -------------------------------------------------------------------------------- /samples/browser-app-sample.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-cloud-security/appid-serversdk-nodejs/0d0fc1896edddf01707f44dea4c706917d94791f/samples/browser-app-sample.js -------------------------------------------------------------------------------- /samples/cloud-directory-app-sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CloudDirectoryAppSample 6 | 7 | 9 | 10 | 33 | 47 | 48 | 49 |

This is CLOUD LAND, cloud directory sample app for appid-serversdk-nodejs

50 |

It demonstrates how to use appid-serversdk-nodejs sign-in, sign-up, forgot password and resend notification in a 51 | server-side web application.

52 |
53 |
54 | 56 |
57 |
58 |

You're not authenticated :(

59 |
60 | 61 |
62 |

You're authenticated :)

63 |

Hello

64 |

Your email is

65 |

66 | 67 | 68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /samples/custom-identity-app-sample-server.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const express = require('express'); 15 | const session = require('express-session'); 16 | const helmet = require('helmet'); 17 | const cookieParser = require('cookie-parser'); 18 | const log4js = require('log4js'); 19 | const logger = log4js.getLogger('custom-identity-sample-app'); 20 | const jwt = require('jsonwebtoken'); 21 | const fs = require('fs'); 22 | 23 | const AppID = require('../lib/appid-sdk'); 24 | const app = express(); 25 | app.use(express.json()); 26 | app.use(express.urlencoded({ extended: false })); 27 | app.use(helmet()); 28 | app.use(cookieParser()); 29 | app.use(express.static(__dirname)); 30 | app.set('view engine', 'ejs'); 31 | app.use(session({ 32 | secret: '123456', 33 | resave: true, 34 | saveUninitialized: true 35 | })); 36 | 37 | const LOGIN_URL = '/login'; 38 | const PROTECTED_URL = '/protected'; 39 | const APPID_AUTH_CONTEXT = 'AppID_Auth_context'; 40 | 41 | const tokenManager = new AppID.TokenManager({ 42 | clientId: '{client-id}', 43 | secret: '{secret}', 44 | oauthServerUrl: '{oauth-server-url}', 45 | }); 46 | 47 | app.get(LOGIN_URL, (req, res) => { 48 | res.render('custom_identity_login', { message: null }); 49 | }); 50 | 51 | app.post(LOGIN_URL, (req, res) => { 52 | if (req.body.username === req.body.password) { 53 | 54 | const sampleToken = { 55 | header: { 56 | alg: 'RS256', 57 | kid: 'sample-rsa-private-key' 58 | }, 59 | payload: { 60 | iss: 'sample-appid-custom-identity', 61 | sub: 'sample-unique-user-id', 62 | aud: tokenManager.serviceConfig.getOAuthServerUrl().split('/')[2], 63 | exp: 9999999999, 64 | name: req.body.username, 65 | scope: 'customScope' 66 | } 67 | }; 68 | 69 | const generateSignedJWT = (privateKey) => { 70 | const { header, payload } = sampleToken; 71 | return jwt.sign(payload, privateKey, { header }); 72 | }; 73 | 74 | const privateKey = fs.readFileSync('./resources/private.pem'); 75 | jwsTokenString = generateSignedJWT(privateKey); 76 | 77 | logger.info(`Generated JWS: ${jwsTokenString}`); 78 | logger.debug('Calling tokenManager.getCustomIdentityTokens()'); 79 | 80 | tokenManager.getCustomIdentityTokens(jwsTokenString).then((authContext) => { 81 | // authContext.accessToken: Access token string 82 | // authContext.identityToken: Identity token string 83 | // authContext.tokenType: Type of tokens 84 | // authContext.expiresIn: Expiry of tokens 85 | 86 | logger.info(`Access token string: ${authContext.accessToken}`); 87 | logger.info(`Identity token string: ${authContext.identityToken}`); 88 | 89 | req.session[APPID_AUTH_CONTEXT] = authContext; 90 | req.session[APPID_AUTH_CONTEXT].identityTokenPayload = jwt.decode(authContext.identityToken); 91 | req.session[APPID_AUTH_CONTEXT].accessTokenPayload = jwt.decode(authContext.accessToken); 92 | 93 | res.redirect(PROTECTED_URL); 94 | }).catch((error) => { 95 | res.render('custom_identity_login', { message: error }); 96 | }); 97 | 98 | } else { 99 | res.render('custom_identity_login', { message: 'Login Failed' }); 100 | } 101 | }); 102 | 103 | app.get(PROTECTED_URL, (req, res) => { 104 | const appIdAuthContext = req.session[APPID_AUTH_CONTEXT]; 105 | const username = appIdAuthContext.identityTokenPayload.name; 106 | res.send(`Hello ${username}, This is a protected resource`); 107 | }); 108 | 109 | const port = process.env.PORT || 3000; 110 | 111 | app.listen(port, () => { 112 | logger.info(`Listening on http://localhost:${port}`); 113 | }); 114 | -------------------------------------------------------------------------------- /samples/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-cloud-security/appid-serversdk-nodejs/0d0fc1896edddf01707f44dea4c706917d94791f/samples/images/bg.png -------------------------------------------------------------------------------- /samples/images/logo_cloud_land.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-cloud-security/appid-serversdk-nodejs/0d0fc1896edddf01707f44dea4c706917d94791f/samples/images/logo_cloud_land.png -------------------------------------------------------------------------------- /samples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Samples 6 | 7 | 8 |

You're probably looking for one of these

9 |

web-app-sample.html

10 |

browser-app-sample.html

11 | 12 | -------------------------------------------------------------------------------- /samples/resources/private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAvt40N1wRsBuaevUevbf7gwRVOUrVnt2Z653Ysi8wzPyxmcC5W0z3wThC 3 | Y7UE2S2kSCf5TY4WDgg1djEUnI6y8HtO0j4bk+U5oVteN51oIp1nHxvdSxR/q4sm+U0Oveg5 4 | MZSxoRN4Hj3T3W7fZWbYf6hypCO/rPBprWBFog8m7xJa9rcdwF2bBPjkXZ5YkztbLGwYp+Xu 5 | 1tHUtMdtuyElKZr6bca+14xLoKqqWipHnqqvm+j9OVjbxDVJ8NxUhwWC/F7unBsboK2PoQAD 6 | OF4upmadnPOeLWYJjFERR97lGs4rdW23VaVmx+aPQypU4Ldf92WiMlDaGCZYdQZtIfGTCwID 7 | AQABAoIBABcseWCDUC52m4H0QuA791CH9ST4ngkk1KxTxNyVeDJbN+D4EkkwhiAyiXT7pHQN 8 | 45VTLSSqbKkVfBA2k08S0Ez8W6yRF94UBbQ+mKjd9qfFPqD7TBnyijG0um2oWnLaQB9x80Mg 9 | TwQ084U+EeeI3h5ZwFmHcZ04r0v6lVnAxzRda3ctfxqGVr6yBsZPTjyRcCDJD1w1C5arAW3I 10 | Jq0qRHRcX5FUOzI5xHb8qm05gbkf4++ueZeRfFHKHhBk5jybnAaxINMP07oPA6jEG1ulZKqJ 11 | UAmNhIuQxRzHZrXTkw0Vrzf1JaTDh9CLGfWg36N1+0xgF9yQmEEF7uhALZ4bkDECgYEA4hgN 12 | 25iHWn/N2Dc1Ysl3PTujB7/IFXL2jID8jaEmnYSZ+r7/z/OSGn6415uVzvlwT6qgNbTWAT3Q 13 | NasPTE9fDnZt7f6PKta5wyBOCLkd/XZgmm6CXPO4u3YeSBYUhBAbrDswgz8tDqM+4yZqsbo+ 14 | csvk03o/5wT8qEytHMrRozkCgYEA2B1V0zDESnXks8rX3/Jg+EJF/STYlmIRZM+WWrwJFZJA 15 | q9mCWxLKQ6PSketmJNjID/lxZ6xOes05vzkgPkBn/LOW/4PyvWmSEQnCajWda1OLB268/JkH 16 | A3XEg3frGlYu9tujjvJuSam5k5KzLYPqLtLKL7MAGML7XICIOegXFGMCgYBhdI7cngWR6871 17 | qO55E3AzU7Z1S6RaIoDFlX/HKLR1Z0fb/mJT56CNjRvty+GqInjXzitamwU36cYKrB5e/UNy 18 | /3dpA7YAeCgARLd+KRUVQOZpCsNkf6WcIFDzL5lOR4c2GRlTKXMpgRJFZTTOQQJUBzEuOt66 19 | nLqvbMWdyIrOeQKBgD8Hv0JLFNTKsZsma/Oq01FUsujz2B90FrKaQLXR5axe7XGxjG1Xe5pi 20 | q0VXrIDOoPrXu5WLEZCLTm5REPBXBH0VO9Ll/uPzaGCDwioL3Q+yW/gc+g2J7Bu8O0ZEsVML 21 | E8N3p6pHVpcxYyiBPrTlpmVloQZsZHjUVU9TRWfuIt0VAoGAFemu+udYoM1XudQGFf30F2Fn 22 | HtaxEgRvN0T24iKMpTHLf5K+mGF7MpFQgb0TMpVeGeOZu/LMJBlqZLsBTpcMGotxUYMEXEA5 23 | uDFICm49uK+ru6r7fgT/7KWglQEvH0jMJ8LQRarLiIjAwO3AAnyatKlIespd9p1VTNUjDVbM 24 | 5J0= 25 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /samples/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "en", 3 | 4 | "emailLabel": "Email", 5 | "passwordLabel": "Password", 6 | "loginLabel": "Login", 7 | "forgotPasswordLink": "Forgot Password?", 8 | "DontHaveLabel": "Don't have account?", 9 | "signUpLink": "Sign up", 10 | 11 | "firstNameLabel": "First Name", 12 | "lastNameLabel": "Last Name", 13 | "phoneLabel": "Phone Number", 14 | "rePasswordLabel": "Re-enter password", 15 | 16 | "resendNotificationButtonText": "Resend notification", 17 | "signUpThanksHeader": "Thanks for signing up", 18 | "signUpThanksText1": "A confirmation of your email is on", 19 | "signUpThanksText2": "its way to", 20 | "signUpThanksText3": "After you confirm, you can sign-in", 21 | "signUpThanksText4": "to the app!", 22 | "text1": "If you don't see our email, check your", 23 | "text2": "spam folder. If you didn't receive our", 24 | "text3": "email, click Resend and we'll send", 25 | "text4": "another.", 26 | 27 | "confirmText1": "You can now access", 28 | "confirmText2": "to the app!", 29 | "confirmHeader1": "Your account is now confirmed!", 30 | "confirmHeader2": "Expired confirmation code", 31 | "confirmHeader3": "Your account already confirmed", 32 | 33 | "forgotPasswordLabel": "Forgot Password", 34 | "ResetPasswordButton": "Reset Password", 35 | "checkYourMail": "Check your mail", 36 | "resetLinkWasSent": "Reset link was sent", 37 | 38 | "resetPasswordLabel": "Reset Password", 39 | "saveChangesButton": "Save Changes", 40 | "newPassword": "New password", 41 | "reEnterNewPassword": "Re-Enter new password", 42 | "currentPassword": "Current password", 43 | 44 | "expiredMsg": "This reset password request expired, please start new forgot password request", 45 | 46 | "passwordChanged1": "Your password has been changed", 47 | "passwordChanged2": "Your password has been changed for your account", 48 | "passwordChanged3": "You can now sign-in with your new password.", 49 | 50 | "changeDetails": "Change Details", 51 | "changePassword": "Change Password", 52 | 53 | "messages": { 54 | "sent": "We sent notification again", 55 | "confirmed": "Account already confirmed", 56 | "tryLater": "Try later" 57 | }, 58 | "errors": { 59 | "uniqueness": "Email already exists", 60 | "forbidden": "Please confirm your account first", 61 | "invalid_grant": "wrong username or password", 62 | "passwords_mismatch": "passwords are not the same", 63 | "invalidValue":"Password must be at least 8 characters", 64 | "general_error": "Something went wrong, try again", 65 | "userNotFound": "We don't recognize this email", 66 | "incorrect_password": "Incorrect current password" 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /samples/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "fr", 3 | "emailLabel": "Email", 4 | "passwordLabel": "Mot de passe", 5 | "loginLabel": "S'identifier", 6 | "forgotPasswordLink": "Mot de passe oublié?", 7 | "DontHaveLabel": "Vous n'avez pas de compte?", 8 | "signUpLink": "S'inscrire", 9 | "firstNameLabel": "Prénom", 10 | "lastNameLabel": "Nom de famille", 11 | "phoneLabel": "Numéro de téléphone", 12 | "rePasswordLabel": "Retaper le mot de passe", 13 | "resendNotificationButtonText": "Renvoyer la notification", 14 | "signUpThanksHeader": "Merci pour l'enregistrement", 15 | "signUpThanksText1": "Une confirmation de votre email est sur", 16 | "signUpThanksText2": "son chemin à", 17 | "signUpThanksText3": "Après confirmation, vous pouvez vous connecter", 18 | "signUpThanksText4": "à l'application!", 19 | "text1": "Si vous ne voyez pas notre email, vérifiez votre", 20 | "text2": "dossier de spam. Si vous n'avez pas reçu notre", 21 | "text3": "email, cliquez sur Renvoyer et nous vous enverronsd", 22 | "text4": "un autre.", 23 | "confirmText1": "Vous pouvez maintenant accéder", 24 | "confirmText2": "à l'application!", 25 | "confirmHeader1": "Votre compte est maintenant confirmé!", 26 | "confirmHeader2": "Code de confirmation expiré", 27 | "confirmHeader3": "Votre compte a déjà été confirmé", 28 | 29 | "forgotPasswordLabel": "Mot de passe oublié", 30 | "ResetPasswordButton": "réinitialiser le mot de passe", 31 | "checkYourMail": "Vérifier votre courrier", 32 | "resetLinkWasSent": "Le lien de réinitialisation a été envoyé", 33 | 34 | "resetPasswordLabel": "réinitialiser le mot de passe", 35 | "saveChangesButton": "Sauvegarder les modifications", 36 | "newPassword": "Nouveau mot de passe", 37 | "reEnterNewPassword": "Ré-entrez le nouveau mot de passe", 38 | "currentPassword": "Mot de passe actuel", 39 | 40 | "expiredMsg": "Cette demande de réinitialisation du mot de passe a expiré, veuillez lancer une nouvelle demande de mot de passe oublié", 41 | 42 | "passwordChanged1": "votre mot de passe a été changé", 43 | "passwordChanged2": "Votre mot de passe a été modifié pour votre compte", 44 | "passwordChanged3": "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.", 45 | 46 | 47 | "changeDetails": "Modifier les détails", 48 | "changePassword": "Changer le mot de passe", 49 | 50 | "messages": { 51 | "sent": "Nous avons envoyé une nouvelle notification", 52 | "confirmed": "Compte déjà confirmé", 53 | "tryLater": "Essayer plus tard" 54 | }, 55 | "errors": { 56 | "uniqueness": "l'email existe déjà", 57 | "forbidden": "Veuillez d'abord confirmer votre compte", 58 | "invalid_grant": "mauvais nom d'utilisateur ou mot de passe", 59 | "passwords_mismatch": "les mots de passe ne sont pas les mêmes", 60 | "invalidValue":"Mot de passe doit être d'au moins 8 caractères", 61 | "general_error": "Quelque chose s'est mal passé essaie encore", 62 | "userNotFound": "Nous ne reconnaissons pas cet e-mail", 63 | "incorrect_password": "Mot de passe actuel incorrect" 64 | } 65 | } -------------------------------------------------------------------------------- /samples/views/account_confirmed.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node Authentication 5 | 6 | 7 | 8 | 9 | 86 | 87 | 88 |
89 |
90 |
91 |
92 | 94 |
95 | 96 |
97 |

<%= confirmText1 %>
<%= confirmText2 %>

98 |
99 |
100 | 101 |
102 | 121 |
122 |
123 | 124 |
125 | 156 | 157 | -------------------------------------------------------------------------------- /samples/views/cd_login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node Authentication 5 | 6 | 7 | 8 | 9 | 24 | 25 | 26 |
27 |
28 |
29 | 31 |
32 |

<%= loginLabel %>

33 | 34 | <% if (message.length > 0) { %> 35 |
<%= message %>
36 | <% } %> 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 | <%= forgotPasswordLink %> 45 | 46 | 47 |
48 | 49 | 50 |
<%= DontHaveLabel %> 51 | <%= signUpLink %> 52 |
53 |
54 | 55 | 59 |
60 |
61 | 62 | 68 | -------------------------------------------------------------------------------- /samples/views/change_details.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Change details 5 | 6 | 7 | 19 | 20 | 21 |
22 | 23 |
24 | 25 |

<%= changeDetails %>

26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 | 45 | 46 | 47 |
48 | 49 |
50 | 51 |
52 | 53 | -------------------------------------------------------------------------------- /samples/views/change_password.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Change Password 4 | 5 | 6 | 7 | 8 | 9 | 26 | 27 | 28 |
29 |
30 |
31 | 34 |
35 |

<%= changePassword %>

36 | 37 | 38 | <% if (message && message.length > 0) { %> 39 |
<%= message %>
40 | <% } %> 41 | 42 |
46 |
47 | 48 | 52 |
53 |
54 |
55 | 56 | 60 |
61 |
62 | 63 |
64 | 65 | 69 |
70 |
71 | 72 | 73 |
74 | 75 | 79 |
80 |
81 | 82 |
83 |
84 | 92 | 93 | -------------------------------------------------------------------------------- /samples/views/custom_identity_login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Custom Identity Sample App 5 | 7 | 10 | 15 | 16 | 17 |
18 | 19 |
20 | 21 |

Login

22 | 23 | <% if (message) { %> 24 |
<%= message %>
25 | <% } %> 26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 | 36 | 37 |
38 | 39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /samples/views/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node Authentication 5 | 6 | 7 | 10 | 11 | 12 |
13 | 14 |
15 | 16 |

Login

17 | 18 | 19 | <% if (message.length > 0) { %> 20 |
<%= message %>
21 | <% } %> 22 | 23 | 24 |
25 |
26 | 27 | 28 |
29 | 30 |
31 | Forgot Password? 32 | 33 | 34 |
35 | 36 | 37 |
38 |
Don't have an account? 39 | Sign up 40 |
41 | 42 |
43 | 44 |

Or go home.

45 | 46 |
47 |
48 | 49 | -------------------------------------------------------------------------------- /samples/views/reset_password_expired.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node Authentication 5 | 6 | 7 | 8 | 9 | 10 | 11 | 33 | 34 | 35 |
36 |
37 |
38 | 41 |
42 | 43 |

<%= expiredMsg %>

44 | 45 |
46 | 47 |
48 | 49 | -------------------------------------------------------------------------------- /samples/views/reset_password_form.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Reset Password Request 4 | 5 | 6 | 7 | 8 | 9 | 26 | 27 | 28 |
29 |
30 |
31 | 34 |
35 |

<%= resetPasswordLabel %>

36 | 37 | 38 | <% if (message && message.length > 0) { %> 39 |
<%= message %>
40 | <% } %> 41 | 42 |
46 |
47 | 48 | 52 |
53 |
54 | 55 |
56 | 57 | 61 |
62 |
63 | 64 | 65 |
66 | 67 | 68 | 72 |
73 |
74 | 75 |
76 |
77 | 85 | 86 | -------------------------------------------------------------------------------- /samples/views/reset_password_sent.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node Authentication 5 | 6 | 7 | 8 | 9 | 82 | 83 | 84 |
85 |
86 | 87 |
88 | 89 |
90 |
91 |

<%= resetLinkWasSent %>

92 |
93 |

<%= checkYourMail %>

94 |

'<%= email %>'

95 |
96 |
97 | 98 | 99 |
100 | 105 |
106 |
107 |

<%= text1 %>
108 | <%= text2 %>
109 | <%= text3 %>
110 | <%= text4 %>

111 |
112 |
113 | 114 |
115 | 134 | 135 | -------------------------------------------------------------------------------- /samples/views/reset_password_success.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Password changed 4 | 5 | 6 | 7 | 8 | 9 | 30 | 31 | 32 |
33 |
34 |
35 | 37 |
38 |

<%= passwordChanged1 %>

39 |

<%= passwordChanged2 %>

40 |

<%= email %>

41 |
42 |
43 |

<%= passwordChanged3 %>

44 | 45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /samples/views/self_forgot_password.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forgot Password 5 | 6 | 7 | 19 | 20 | 21 |
22 |
23 |

<%= forgotPasswordLabel %>

24 | 25 | <% if (message && message.length > 0) { %> 26 |
<%= message %>
27 | <% } %> 28 | 29 |
30 |
31 | 32 | 33 |
34 | 35 | 36 |
37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /samples/views/self_sign_up.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sign up 5 | 6 | 7 | 19 | 20 | 21 |
22 | 23 |
24 | 25 |

<%= signUpLink %>

26 | 27 | 28 | <% if (message && message.length > 0) { %> 29 |
<%= message %>
30 | <% } %> 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 | 51 | 52 |
53 |
54 | 55 | 56 |
57 | 58 | 59 | 60 |
61 | 62 |
63 | 64 |
65 | 66 | -------------------------------------------------------------------------------- /samples/views/thanks_for_sign_up.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node Authentication 5 | 6 | 7 | 8 | 9 | 82 | 83 | 84 |
85 |
86 |
87 |

<%= signUpThanksHeader %> <%= displayName %>!

88 |
89 |

<%= signUpThanksText1 %>
<%= signUpThanksText2 %>

90 |

'<%= email %>'

91 |
92 |

<%= signUpThanksText3 %>
<%= signUpThanksText4 %>

93 |
94 |
95 |
96 | 97 | 98 |
99 | 104 |
105 |
106 |

<%= text1 %>
107 | <%= text2 %>
108 | <%= text3 %>
109 | <%= text4 %>

110 |
111 |
112 | 113 |
114 | 133 | 134 | -------------------------------------------------------------------------------- /samples/web-app-sample-server.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const express = require("express"); 15 | const session = require("express-session"); 16 | const log4js = require("log4js"); 17 | const passport = require("passport"); 18 | const WebAppStrategy = require("./../lib/appid-sdk").WebAppStrategy; 19 | const helmet = require("helmet"); 20 | const bodyParser = require("body-parser"); // get information from html forms 21 | const cookieParser = require("cookie-parser"); 22 | const flash = require("connect-flash"); 23 | const app = express(); 24 | const logger = log4js.getLogger("testApp"); 25 | 26 | // Below URLs will be used for App ID OAuth flows 27 | const LANDING_PAGE_URL = "/web-app-sample.html"; 28 | const LOGIN_URL = "/ibm/bluemix/appid/login"; 29 | const SIGN_UP_URL = "/ibm/bluemix/appid/sign_up"; 30 | const CHANGE_PASSWORD_URL = "/ibm/bluemix/appid/change_password"; 31 | const CHANGE_DETAILS_URL = "/ibm/bluemix/appid/change_details"; 32 | const FORGOT_PASSWORD_URL = "/ibm/bluemix/appid/forgot_password"; 33 | const LOGIN_ANON_URL = "/ibm/bluemix/appid/loginanon"; 34 | const CALLBACK_URL = "/ibm/bluemix/appid/callback"; 35 | const LOGOUT_URL = "/ibm/bluemix/appid/logout"; 36 | const ROP_LOGIN_PAGE_URL = "/ibm/bluemix/appid/rop/login"; 37 | 38 | app.use(helmet()); 39 | app.use(flash()); 40 | app.use(cookieParser()); 41 | app.set('view engine', 'ejs'); // set up ejs for templating 42 | 43 | // Setup express application to use express-session middleware 44 | // Must be configured with proper session storage for production 45 | // environments. See https://github.com/expressjs/session for 46 | // additional documentation. 47 | 48 | // Also, if you plan on explicitly stating cookie usage with the 49 | // "sameSite" attribute, you can set the value to "Lax" or "None" 50 | // depending on your preferences. However, note that setting the 51 | // value to "true" will assign the value "Strict" to the sameSite 52 | // attribute which will result into an authentication error because 53 | // setting the "Strict" value will cause your browser not to send your 54 | // cookies after the redirect that happens during the authentication process. 55 | 56 | app.use(session({ 57 | secret: "123456", 58 | resave: true, 59 | saveUninitialized: true 60 | })); 61 | 62 | // Use static resources from /samples directory 63 | app.use(express.static(__dirname)); 64 | 65 | // Configure express application to use passportjs 66 | app.use(passport.initialize()); 67 | app.use(passport.session()); 68 | 69 | // Configure passportjs to use WebAppStrategy 70 | let webAppStrategy = new WebAppStrategy({ 71 | tenantId: "TENANT_ID", 72 | clientId: "CLIENT_ID", 73 | secret: "SECRET", 74 | oauthServerUrl: "OAUTH_SERVER_URL", 75 | redirectUri: "http://localhost:3000" + CALLBACK_URL 76 | }); 77 | passport.use(webAppStrategy); 78 | 79 | // Configure passportjs with user serialization/deserialization. This is required 80 | // for authenticated session persistence accross HTTP requests. See passportjs docs 81 | // for additional information http://passportjs.org/docs 82 | passport.serializeUser(function (user, cb) { 83 | cb(null, user); 84 | }); 85 | 86 | passport.deserializeUser(function (obj, cb) { 87 | cb(null, obj); 88 | }); 89 | 90 | // Explicit login endpoint. Will always redirect browser to login widget due to {forceLogin: true}. 91 | // If forceLogin is set to false redirect to login widget will not occur of already authenticated users. 92 | app.get(LOGIN_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, { 93 | successRedirect: LANDING_PAGE_URL, 94 | forceLogin: true 95 | })); 96 | 97 | // Explicit forgot password endpoint. Will always redirect browser to forgot password widget screen. 98 | app.get(FORGOT_PASSWORD_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, { 99 | successRedirect: LANDING_PAGE_URL, 100 | show: WebAppStrategy.FORGOT_PASSWORD 101 | })); 102 | 103 | // Explicit change details endpoint. Will always redirect browser to change details widget screen. 104 | app.get(CHANGE_DETAILS_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, { 105 | successRedirect: LANDING_PAGE_URL, 106 | show: WebAppStrategy.CHANGE_DETAILS 107 | })); 108 | 109 | // Explicit change password endpoint. Will always redirect browser to change password widget screen. 110 | app.get(CHANGE_PASSWORD_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, { 111 | successRedirect: LANDING_PAGE_URL, 112 | show: WebAppStrategy.CHANGE_PASSWORD 113 | })); 114 | 115 | // Explicit sign up endpoint. Will always redirect browser to sign up widget screen. 116 | // default value - false 117 | app.get(SIGN_UP_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, { 118 | successRedirect: LANDING_PAGE_URL, 119 | show: WebAppStrategy.SIGN_UP 120 | })); 121 | 122 | // Explicit anonymous login endpoint. Will always redirect browser for anonymous login due to forceLogin: true 123 | app.get(LOGIN_ANON_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, { 124 | successRedirect: LANDING_PAGE_URL, 125 | allowAnonymousLogin: true, 126 | allowCreateNewAnonymousUser: true 127 | })); 128 | 129 | // Callback to finish the authorization process. Will retrieve access and identity tokens/ 130 | // from App ID service and redirect to either (in below order) 131 | // 1. the original URL of the request that triggered authentication, as persisted in HTTP session under WebAppStrategy.ORIGINAL_URL key. 132 | // 2. successRedirect as specified in passport.authenticate(name, {successRedirect: "...."}) invocation 133 | // 3. application root ("/") 134 | app.get(CALLBACK_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, { keepSessionInfo: true })); 135 | 136 | // Logout endpoint. Clears authentication information from session 137 | app.get(LOGOUT_URL, function (req, res) { 138 | req._sessionManager = false; 139 | WebAppStrategy.logout(req); 140 | res.clearCookie("refreshToken"); 141 | res.redirect(LANDING_PAGE_URL); 142 | }); 143 | 144 | function storeRefreshTokenInCookie(req, res, next) { 145 | const refreshToken = req.session[WebAppStrategy.AUTH_CONTEXT].refreshToken; 146 | if (refreshToken) { 147 | /* An example of storing user's refresh-token in a cookie with expiration of a month */ 148 | res.cookie("refreshToken", refreshToken, { 149 | maxAge: 1000 * 60 * 60 * 24 * 30 /* 30 days */ 150 | }); 151 | } 152 | next(); 153 | } 154 | 155 | function isLoggedIn(req) { 156 | return req.session[WebAppStrategy.AUTH_CONTEXT]; 157 | } 158 | 159 | // Protected area. If current user is not authenticated - redirect to the login widget will be returned. 160 | // In case user is authenticated - a page with current user information will be returned. 161 | app.get("/protected", function tryToRefreshTokenIfNotLoggedIn(req, res, next) { 162 | if (isLoggedIn(req)) { 163 | return next(); 164 | } 165 | 166 | webAppStrategy.refreshTokens(req, req.cookies.refreshToken).then(function () { 167 | next(); 168 | }); 169 | }, passport.authenticate(WebAppStrategy.STRATEGY_NAME, { keepSessionInfo: true }), storeRefreshTokenInCookie, function (req, res) { 170 | logger.debug("/protected"); 171 | res.json(req.user); 172 | }); 173 | 174 | app.post("/rop/login/submit", bodyParser.urlencoded({ 175 | extended: false 176 | }), passport.authenticate(WebAppStrategy.STRATEGY_NAME, { 177 | successRedirect: LANDING_PAGE_URL, 178 | failureRedirect: ROP_LOGIN_PAGE_URL, 179 | failureFlash: true, // allow flash messages 180 | keepSessionInfo: true 181 | })); 182 | 183 | app.get(ROP_LOGIN_PAGE_URL, function (req, res) { 184 | // render the page and pass in any flash data if it exists 185 | res.render("login.ejs", { 186 | message: req.flash('error') 187 | }); 188 | }); 189 | 190 | var port = process.env.PORT || 3000; 191 | 192 | app.listen(port, function () { 193 | logger.info("Listening on http://localhost:" + port + "/web-app-sample.html"); 194 | }); -------------------------------------------------------------------------------- /samples/web-app-sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebAppSample 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

This is a web-app-sample for appid-serversdk-nodejs

14 |

It demonstrates how to use appid-serversdk-nodejs in a server-side web application.

15 |
16 | 17 |
18 |

You're not authenticated :(

19 |
20 | 21 |
22 |

You're authenticated :)

23 |

Hello

24 |

Your userId is

25 |



26 |
27 | 28 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /samples/web-app-sample.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | $(".hideOnStartup").hide(); 3 | 4 | $.getJSON("/protected", function(data){ 5 | // Already authenticated 6 | $("#WhenAuthenticated").show(); 7 | $("#sub").text(data.sub); 8 | $("#name").text(data.name || "Anonymous"); 9 | $("#picture").attr("src", data.picture || ""); 10 | }).fail(function(){ 11 | // Not authenticated yet 12 | $("#WhenNotAuthenticated").show(); 13 | }).always(function(){ 14 | $("#LoginButtons").show(); 15 | }); 16 | }); -------------------------------------------------------------------------------- /test/api-strategy-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const chai = require("chai"); 15 | const assert = chai.assert; 16 | const proxyquire = require("proxyquire"); 17 | 18 | describe("/lib/strategies/api-strategy", function () { 19 | console.log("Loading api-strategy-test.js"); 20 | 21 | var APIStrategy = proxyquire("../lib/strategies/api-strategy", { 22 | "../utils/public-key-util": require("./mocks/public-key-util-mock"), 23 | "../utils/token-util": require("./mocks/token-util-mock") 24 | }); 25 | var apiStrategy= new APIStrategy({ 26 | oauthServerUrl: "serverUrl" 27 | }); 28 | 29 | 30 | describe("#properties", function () { 31 | it("Should have all properties", function (done) { 32 | assert.isFunction(APIStrategy); 33 | assert.equal(APIStrategy.STRATEGY_NAME, "appid-api-strategy"); 34 | assert.equal(APIStrategy.DEFAULT_SCOPE, "appid_default"); 35 | done(); 36 | }); 37 | }); 38 | 39 | describe("#authenticate()", function () { 40 | 41 | it("Should fail returning both default and custom scopes", function (done) { 42 | apiStrategy.fail = function (msg, status) { 43 | assert.equal(msg, "Bearer scope=\"appid_default custom_scope\", error=\"invalid_token\""); 44 | assert.equal(status, 401); 45 | done(); 46 | }; 47 | 48 | apiStrategy.authenticate({ 49 | header: function () { 50 | return null; 51 | } 52 | }, { 53 | scope: "custom_scope" 54 | }); 55 | }); 56 | 57 | it("Should fail when there's no access token", function (done) { 58 | apiStrategy.fail = function (msg, status) { 59 | assert.equal(msg, 'Bearer scope="appid_default", error="invalid_token"'); 60 | assert.equal(status, 401); 61 | done(); 62 | }; 63 | 64 | apiStrategy.authenticate({ 65 | header: function () { 66 | return null; 67 | } 68 | }); 69 | }); 70 | 71 | it("Should fail when access token is not Bearer", function (done) { 72 | apiStrategy.fail = function (msg, status) { 73 | assert.equal(msg, 'Bearer scope="appid_default", error="invalid_token"'); 74 | assert.equal(status, 401); 75 | done() 76 | }; 77 | apiStrategy.authenticate({ 78 | header: function () { 79 | return "Some Weird Stuff"; 80 | } 81 | }); 82 | }); 83 | 84 | it("Should fail when access token is malformed", function (done) { 85 | apiStrategy.fail = function (msg, status) { 86 | assert.equal(msg, 'Bearer scope="appid_default", error="invalid_token"'); 87 | assert.equal(status, 401); 88 | done() 89 | }; 90 | apiStrategy.authenticate({ 91 | header: function () { 92 | return "Bearer asd asd asd"; 93 | } 94 | }); 95 | }); 96 | 97 | it("Should fail when access token cannot be decoded", function (done) { 98 | apiStrategy.fail = function (msg, status) { 99 | assert.equal(msg, 'Bearer scope="appid_default", error="invalid_token"'); 100 | assert.equal(status, 401); 101 | done(); 102 | } 103 | apiStrategy.authenticate({ 104 | header: function () { 105 | return "Bearer invalid_token"; 106 | } 107 | }); 108 | }); 109 | 110 | it("Should fail when access token scope does not contain required scope", function (done) { 111 | apiStrategy.fail = function (msg, status) { 112 | assert.equal(msg, 'Bearer scope="appid_default", error="insufficient_scope"'); 113 | assert.equal(status, 401); 114 | done(); 115 | } 116 | apiStrategy.authenticate({ 117 | header: function () { 118 | return "Bearer bad_scope"; 119 | } 120 | }); 121 | }); 122 | 123 | it("Should not fail when id token is not present", function (done) { 124 | var req = { 125 | header: function () { 126 | return "Bearer access_token"; 127 | } 128 | }; 129 | 130 | apiStrategy.success = function (idToken) { 131 | assert.isNull(idToken); 132 | assert.isObject(req.appIdAuthorizationContext); 133 | 134 | assert.isString(req.appIdAuthorizationContext.accessToken); 135 | assert.equal(req.appIdAuthorizationContext.accessToken, "access_token"); 136 | assert.isObject(req.appIdAuthorizationContext.accessTokenPayload); 137 | assert.equal(req.appIdAuthorizationContext.accessTokenPayload.scope, "appid_default"); 138 | 139 | assert.isUndefined(req.appIdAuthorizationContext.identityToken); 140 | assert.isUndefined(req.appIdAuthorizationContext.identityTokenPayload); 141 | 142 | done(); 143 | }; 144 | 145 | apiStrategy.authenticate(req); 146 | }); 147 | 148 | it("Should not fail when id token is invalid", function (done) { 149 | var req = { 150 | header: function () { 151 | return "Bearer access_token invalid_token"; 152 | } 153 | }; 154 | 155 | apiStrategy.success = function (idToken) { 156 | assert.isNull(idToken); 157 | assert.isObject(req.appIdAuthorizationContext); 158 | 159 | assert.isString(req.appIdAuthorizationContext.accessToken); 160 | assert.equal(req.appIdAuthorizationContext.accessToken, "access_token"); 161 | assert.isObject(req.appIdAuthorizationContext.accessTokenPayload); 162 | assert.equal(req.appIdAuthorizationContext.accessTokenPayload.scope, "appid_default"); 163 | 164 | assert.isUndefined(req.appIdAuthorizationContext.identityToken); 165 | assert.isUndefined(req.appIdAuthorizationContext.identityTokenPayload); 166 | 167 | done(); 168 | }; 169 | 170 | apiStrategy.authenticate(req); 171 | }); 172 | 173 | it("Should succeed when valid access and id tokens are present", function (done) { 174 | var req = { 175 | header: function () { 176 | return "Bearer access_token id_token"; 177 | } 178 | }; 179 | 180 | apiStrategy.success = function (idToken) { 181 | assert.isObject(req.appIdAuthorizationContext); 182 | 183 | assert.isString(req.appIdAuthorizationContext.accessToken); 184 | assert.equal(req.appIdAuthorizationContext.accessToken, "access_token"); 185 | assert.isObject(req.appIdAuthorizationContext.accessTokenPayload); 186 | assert.equal(req.appIdAuthorizationContext.accessTokenPayload.scope, "appid_default"); 187 | 188 | assert.isString(req.appIdAuthorizationContext.identityToken); 189 | assert.equal(req.appIdAuthorizationContext.identityToken, "id_token"); 190 | assert.isObject(req.appIdAuthorizationContext.identityTokenPayload); 191 | assert.equal(req.appIdAuthorizationContext.identityTokenPayload.scope, "appid_default"); 192 | 193 | assert.isObject(idToken); 194 | 195 | assert.equal(idToken.scope, "appid_default"); 196 | done(); 197 | }; 198 | 199 | apiStrategy.authenticate(req); 200 | }); 201 | 202 | it("should succeed when authenticating with 3 scopes, 2 of which are the required scopes", function (done) { 203 | apiStrategy.success = function (idToken) { 204 | assert.equal(req.appIdAuthorizationContext.accessTokenPayload.scope, "appid_default scope1 scope2 scope3"); 205 | assert.equal(idToken.scope, "appid_default scope1 scope2 scope3"); 206 | done(); 207 | }; 208 | 209 | let req = { 210 | header: function () { 211 | return "Bearer access_token_3_scopes id_token_3_scopes"; 212 | } 213 | }; 214 | apiStrategy.authenticate(req, 215 | { 216 | scope: "scope1 scope2", 217 | audience: "myClientId" 218 | }); 219 | }); 220 | 221 | it("should fail when authenticating without the required scopes", function (done) { 222 | apiStrategy.fail = function (msg, status) { 223 | assert.equal(msg, 'Bearer scope="appid_default scope1 scope2", error="insufficient_scope"'); 224 | assert.equal(status, 401); 225 | done(); 226 | }; 227 | 228 | let req = { 229 | header: function () { 230 | return "Bearer access_token id_token"; 231 | } 232 | }; 233 | apiStrategy.authenticate(req, 234 | { 235 | scope: "scope1 scope2", 236 | audience: "myCliendId" 237 | }); 238 | }); 239 | 240 | it("should fail when authenticating without the required scopes", function (done) { 241 | apiStrategy.fail = function (msg, status) { 242 | assert.equal(msg, 'Bearer scope="appid_default scope1 scope2 scope3", error="insufficient_scope"'); 243 | assert.equal(status, 401); 244 | done(); 245 | }; 246 | 247 | let req = { 248 | header: function () { 249 | return "Bearer access_token id_token"; 250 | } 251 | }; 252 | apiStrategy.authenticate(req, 253 | { 254 | scope: "scope1 scope2 scope3" 255 | }); 256 | }); 257 | 258 | it("should succeed when authenticating with *whitespace* as required scopes", function (done) { 259 | apiStrategy.success = function (idToken) { 260 | assert.equal(req.appIdAuthorizationContext.accessTokenPayload.scope, "appid_default scope1 scope2 scope3"); 261 | assert.equal(idToken.scope, "appid_default scope1 scope2 scope3"); 262 | done(); 263 | }; 264 | 265 | let req = { 266 | header: function () { 267 | return "Bearer access_token_3_scopes id_token_3_scopes"; 268 | } 269 | }; 270 | apiStrategy.authenticate(req, 271 | { 272 | scope: " " 273 | }); 274 | }); 275 | 276 | it("should succeed when authenticating with the required scopes, while not passing audience", function (done) { 277 | apiStrategy.success = function (idToken) { 278 | assert.equal(req.appIdAuthorizationContext.accessTokenPayload.scope, "appid_default scope1 scope2 scope3"); 279 | assert.equal(idToken.scope, "appid_default scope1 scope2 scope3"); 280 | done(); 281 | }; 282 | 283 | let req = { 284 | header: function () { 285 | return "Bearer access_token_3_scopes id_token_3_scopes"; 286 | } 287 | }; 288 | apiStrategy.authenticate(req, 289 | { 290 | scope: "scope1 scope2" 291 | }); 292 | }); 293 | 294 | it("should fail with BAD_REQUEST when the required scope is not a string", function (done) { 295 | apiStrategy.fail = function (msg, status) { 296 | assert.equal(status, 400); 297 | done(); 298 | }; 299 | 300 | let req = { 301 | header: function () { 302 | return "Bearer access_token id_token"; 303 | } 304 | }; 305 | apiStrategy.authenticate(req, 306 | { 307 | scope: 42, 308 | audience: "app" 309 | }); 310 | }); 311 | 312 | it("should fail with BAD_REQUEST when the required (non-null) audience is not a string", function (done) { 313 | apiStrategy.fail = function (msg, status) { 314 | assert.equal(status, 400); 315 | done(); 316 | }; 317 | 318 | let req = { 319 | header: function () { 320 | return "Bearer access_token id_token"; 321 | } 322 | }; 323 | apiStrategy.authenticate(req, 324 | { 325 | scope: "scope1", 326 | audience: 42 327 | }); 328 | }); 329 | 330 | it("should fail with BAD_REQUEST when sending several audiences ", function (done) { 331 | apiStrategy.fail = function (msg, status) { 332 | assert(msg.indexOf("multiple audiences are not supported") > 1, true); 333 | assert.equal(status, 400); 334 | done(); 335 | }; 336 | 337 | let req = { 338 | header: function () { 339 | return "Bearer access_token id_token"; 340 | } 341 | }; 342 | apiStrategy.authenticate(req, 343 | { 344 | scope: "appid_default", 345 | audience: "item1 item2" 346 | }); 347 | }); 348 | 349 | it("should fail with AUTH failure when request wrong audience value", function (done) { 350 | apiStrategy.fail = function (msg, status) { 351 | assert(msg.indexOf("audience mismatch") > 1, true); 352 | assert.equal(status, 401); 353 | done(); 354 | }; 355 | 356 | let req = { 357 | header: function () { 358 | return "Bearer access_token_3_scopes id_token"; 359 | } 360 | }; 361 | apiStrategy.authenticate(req, 362 | { 363 | scope: "scope1 scope2", 364 | audience: "myBadClientId" 365 | }); 366 | }); 367 | 368 | }); 369 | }); 370 | -------------------------------------------------------------------------------- /test/appid-sdk-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const chai = require('chai'); 15 | const assert = chai.assert; 16 | 17 | describe('/lib/appid-sdk', function(){ 18 | console.log("Loading appid-sdk-test.js"); 19 | 20 | let AppIdSDK; 21 | 22 | before(function(){ 23 | AppIdSDK = require("../lib/appid-sdk"); 24 | }); 25 | 26 | describe("#AppIdSDK", function(){ 27 | it("Should return WebAppStrategy", (done) => { 28 | assert.isFunction(AppIdSDK.WebAppStrategy); 29 | done(); 30 | }); 31 | 32 | it('Should return APIStrategy', (done) => { 33 | assert.isFunction(AppIdSDK.APIStrategy); 34 | done(); 35 | }); 36 | 37 | it('Should return token manger', (done) => { 38 | assert.isFunction(AppIdSDK.TokenManager); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/common-util-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const chai = require("chai"); 15 | const expect = chai.expect; 16 | const should = chai.should(); 17 | let commonUtil = require("../lib/utils/common-util"); 18 | chai.use(require("chai-as-promised")); 19 | 20 | describe("/lib/utils/common-util", function () { 21 | context('optionalChaining', () => { 22 | const stringName = 'testString'; 23 | const sampleObj = { 24 | "name": "abod", 25 | "age": 30, 26 | "cars": { 27 | "car1": "Ford", 28 | "car2": "BMW", 29 | "car3": "Fiat" 30 | } 31 | } 32 | 33 | it('should successfully return the value of the property', () => { 34 | expect(commonUtil.optionalChaining(() => sampleObj.cars.car2)).to.equal("BMW"); 35 | }); 36 | 37 | it('should return undefined if property is not in the json object', () => { 38 | should.not.exist(commonUtil.optionalChaining(() => sampleObj.cars.car2.wheels)); 39 | }); 40 | 41 | it('should return the same value if a non object was passed', () => { 42 | expect(commonUtil.jsonToURLencodedForm(stringName)).to.equal(stringName); 43 | expect(commonUtil.jsonToURLencodedForm(32)).to.equal(32); 44 | }); 45 | }); 46 | 47 | context('objectKeysToLowerCase', () => { 48 | const mixedKeyCases = { 49 | "First-Name": "abod", 50 | "Age": 30, 51 | "CARS": { 52 | "car1": "Ford", 53 | "car2": "BMW", 54 | "car3": "Fiat" 55 | } 56 | } 57 | const lowerKeyCases = { 58 | "first-name": "abod", 59 | "age": 30, 60 | "cars": { 61 | "car1": "Ford", 62 | "car2": "BMW", 63 | "car3": "Fiat" 64 | } 65 | } 66 | 67 | it('should return the object keys in lowercases', () => { 68 | expect(commonUtil.objectKeysToLowerCase(mixedKeyCases)).to.deep.equal(lowerKeyCases); 69 | }); 70 | }); 71 | 72 | context('jsonToURLencodedForm', () => { 73 | const stringName = 'testString'; 74 | const formData = { 75 | "grant_type": "urn:ibm:params:oauth:grant-type:apikey", 76 | "apikey": "dummyAPIKEY-FCUIw1hgPp31iRjcYllURtWeelFBgHYm4-key" 77 | } 78 | 79 | const urlEncodedData = 'grant_type=urn%3Aibm%3Aparams%3Aoauth%3Agrant-type%3Aapikey&apikey=dummyAPIKEY-FCUIw1hgPp31iRjcYllURtWeelFBgHYm4-key'; 80 | 81 | it('should successfully convert the formData to URLencoded format', () => { 82 | expect(commonUtil.jsonToURLencodedForm(formData)).to.equal(urlEncodedData); 83 | }); 84 | 85 | it('should return the same value if a non object was passed', () => { 86 | expect(commonUtil.jsonToURLencodedForm(stringName)).to.equal(stringName); 87 | expect(commonUtil.jsonToURLencodedForm(32)).to.equal(32); 88 | }); 89 | }); 90 | 91 | context('parseJSON', () => { 92 | const validStringJson = '{"name":"abod","age":28,"car":"ford"}'; 93 | const validJSON = { 94 | "name": "abod", 95 | "age": 28, 96 | "car": "ford" 97 | }; 98 | const htmlError = "
Internal Server Error
"; 99 | 100 | it('should successfully return parsed JSON', () => { 101 | expect(commonUtil.parseJSON(validStringJson)).to.deep.equal(validJSON); 102 | }); 103 | it('should return the exact JSON', () => { 104 | expect(commonUtil.parseJSON(validJSON)).to.deep.equal(validJSON); 105 | }); 106 | 107 | it('should return the exact text - Invalid JSON case', () => { 108 | expect(commonUtil.parseJSON(htmlError)).to.deep.equal(htmlError); 109 | }); 110 | }); 111 | }); -------------------------------------------------------------------------------- /test/mocks/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | module.exports = { 15 | APPID_ALLOW_EXPIRED_TOKENS : "APPID_ALLOW_EXPIRED_TOKENS", 16 | ACCESS_TOKEN : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJhcHBJZC1kYjhhMjdjNC1iODg3LTRmOGQtYTg5Zi1mMTJmYjc3NWIzMTEtMjAxOC0wOC0wMlQxMjowNDowOS43MjgiLCJ2ZXIiOjN9.eyJpc3MiOiJtb2JpbGVjbGllbnRhY2Nlc3Muc3RhZ2UxLm5nLmJsdWVtaXgubmV0IiwiZXhwIjoxNTUyNTEwODMxLCJhdWQiOiIyMWU4YjUyMy1lYjQyLTRhMzQtYTA1Ny0wNGNhOTQ0NWY2ZmYiLCJzdWIiOiIwZTg3NGFkMS0zMmJlLTQ1YjktYTE2YS1mYTI4YjJmMzJmY2QiLCJhbXIiOlsiZ29vZ2xlIl0sImlhdCI6MTU1MjUxMDgyOSwidGVuYW50IjoiZGI4YTI3YzQtYjg4Ny00ZjhkLWE4OWYtZjEyZmI3NzViMzExIiwic2NvcGUiOiJvcGVuaWQgYXBwaWRfZGVmYXVsdCBhcHBpZF9yZWFkcHJvZmlsZSBhcHBpZF9yZWFkdXNlcmF0dHIgYXBwaWRfd3JpdGV1c2VyYXR0ciBhcHBpZF9hdXRoZW50aWNhdGVkIn0.QFz6eP_rW30qb-X15FlbHn526BYzcQMavKOG-cvPKLDiH4VgtX-SXrTx3GSCMIgKe1iZihEkKH9OjgVOsRudv7Jvn3LMz308VVrey0H-MtA-JL5Zhn1ddH5h8rxF3XQdYl60WVmDvDjZNmRm660j4iEYewWhjdAqLgeNbw5EoRv_pqT1F8YcX-lQ_ACuhy3jL4qkB3HS282T26nWiHaRkdn0KmbsDwGIYAPEk7r8ZhAnqBEiUTS2RGczU5fA0HfoJa7utRaN7RpG4hg1MZ3a6N9WW1bhx4SFUer_eTWEf01NpzIEfdtU4F_icH17Jjlci-Qd0QbZFizQ6ueGtjyn1A", 17 | ACCESS_TOKEN_V4: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFwcElkLWRiOGEyN2M0LWI4ODctNGY4ZC1hODlmLWYxMmZiNzc1YjMxMS0yMDE4LTA4LTAyVDEyOjA0OjA5LjcyOCIsInZlciI6NH0.eyJpc3MiOiJodHRwczovL2V1LWdiLmFwcGlkLnRlc3QuY2xvdWQuaWJtLmNvbS9vYXV0aC92NC9kYjhhMjdjNC1iODg3LTRmOGQtYTg5Zi1mMTJmYjc3NWIzMTEiLCJleHAiOjE1NTI1MDI0MjQsImF1ZCI6WyIyMWU4YjUyMy1lYjQyLTRhMzQtYTA1Ny0wNGNhOTQ0NWY2ZmYiXSwic3ViIjoiMGU4NzRhZDEtMzJiZS00NWI5LWExNmEtZmEyOGIyZjMyZmNkIiwiYW1yIjpbImdvb2dsZSJdLCJpYXQiOjE1NTI1MDI0MjIsInRlbmFudCI6ImRiOGEyN2M0LWI4ODctNGY4ZC1hODlmLWYxMmZiNzc1YjMxMSIsInNjb3BlIjoib3BlbmlkIGFwcGlkX2RlZmF1bHQgYXBwaWRfcmVhZHByb2ZpbGUgYXBwaWRfcmVhZHVzZXJhdHRyIGFwcGlkX3dyaXRldXNlcmF0dHIgYXBwaWRfYXV0aGVudGljYXRlZCJ9.YNkhVtNKmL9wForrm1dx3YRzzC291qzlDUKX0VZ9eP8tElec0HtZbuwhk08gyvyBWfXDkQu45kZVYS71f48xgSlKz8O5TLPgGsSZI3agWPccCqjxMcBdfvvkNKNaV3QBAo2dN7SM5K553K_JTzMPFfbaFa0farENfjRWAl7kp9zielmq7C9kkfg8mJCWQwbp5RBdXX-k79-6kNlAnbBAOhWxYM_gz9gu8pxHmfs8RSuRY972FMEEJoE5hdeICE8j1yW113O-QKUTkphFnz7sprx0_6_bvzmDXvYnPIXqGc6d_83iojBGyPXygitp8jO6gfTCTxZvNFQzRYFq1DuQqw", 18 | EXPIRED_ACCESS_TOKEN : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFwcElkLWRiOGEyN2M0LWI4ODctNGY4ZC1hODlmLWYxMmZiNzc1YjMxMS0yMDE4LTA4LTAyVDEyOjA0OjA5LjcyOCIsInZlciI6NH0.eyJpc3MiOiJodHRwczovL2V1LWdiLmFwcGlkLnRlc3QuY2xvdWQuaWJtLmNvbS9vYXV0aC92NC9kYjhhMjdjNC1iODg3LTRmOGQtYTg5Zi1mMTJmYjc3NWIzMTEiLCJleHAiOjE1NTI1MDI0MjAsImF1ZCI6WyIyMWU4YjUyMy1lYjQyLTRhMzQtYTA1Ny0wNGNhOTQ0NWY2ZmYiXSwic3ViIjoiMGU4NzRhZDEtMzJiZS00NWI5LWExNmEtZmEyOGIyZjMyZmNkIiwiYW1yIjpbImdvb2dsZSJdLCJpYXQiOjE1NTI1MDI0MjIsInRlbmFudCI6ImRiOGEyN2M0LWI4ODctNGY4ZC1hODlmLWYxMmZiNzc1YjMxMSIsInNjb3BlIjoib3BlbmlkIGFwcGlkX2RlZmF1bHQgYXBwaWRfcmVhZHByb2ZpbGUgYXBwaWRfcmVhZHVzZXJhdHRyIGFwcGlkX3dyaXRldXNlcmF0dHIgYXBwaWRfYXV0aGVudGljYXRlZCJ9.JQZpZ6sOKNZ1k9stLhpECw55OniWEiqxPJYlOfpWtODwInyzs67JzTjCdTk9BYYk1NCiAFmVtKqskjL7Ud1cyFdOVnbMy4dKlnj4pzSrqmn1RihtL-ieQgalEk_6lHNU744Qm15emwzv2dVOtw7laxGYD4N_bW9CgVW8HW-q6OXBB5dkVDmZaSwP5bHkMu95K18oNKmDHNJLV4eetzXz32ssWQs2aRnMbswJwQHEwfdsQKquOsivUVfZ_8HJyO_Bb_ONQCySAKhW_29mGj7upiQ3WoiWAfLQe3E7ShdWhKK8sGoUQbDH8_GxWfUOKzNmIXjWK4hsO2RXFkdJOIYWFw", 19 | MALFORMED_ACCESS_TOKEN : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtb2JpbGVjbGllbnRhY2Nlc3Muc3RhZ2UxLm5nLmJsdWVtaXgubmV0IiwiZXhwIjoxNDg3MDg0ODc4LCJhdWQiOiIyNmNiMDEyZWIzMjdjNjEyZDkwYTY4MTkxNjNiNmJjYmQ0ODQ5Y2JiIiwiaWF0IjoxNDg3MDgxMjc4LCJhdXRoX2J5IjoiZmFjZWJvb2siLCJ0ZW5hbnQiOiI0ZGJhOTQzMC01NGU2LTRjZjItYTUxNi02ZjczZmViNzAyYmIiLCJzY29wZSI6ImFwcGlkX2RlZmF1bHQgYXBwaWRfcmVhZHByb2ZpbGUgYXBwaWRfcmVhZHVzZXJhdHRyIGFwcGlkX3dyaXRldXNlcmF0dHIifQ.HHterec250JSDY1965cM2DadBznl2wTKmzKNSnfjpdTAqax9VZvV3EwuFbEnGp9-i6AC-OlsVj7xvbALkdjwG2lZvpQx0M_gRc_3E0NiYuOGVolcm0wEXtbtDUFFqZQAf9BYYOPZ8OintdBiwUGETbH1ZRVtUvt3nalIko1OPE1Q12LvuRlhz5MClNHmvxJcXc7kucxCx4s4UFFy_HJA1gow7HWFqc9-PZf4JMWA-siYqPrdw_zYeBTBzE5co92F6JBEtGLLCjhJVz9eYgLLECXbak3z6hOaY9352Weuj7AgMOWxzw56jKKsiixMtvzrCzLVIcRUG96UJszwPHtPlA", 20 | MALFORMED_ACCESS_TOKEN_WITHOUTHEADER : "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp0UifQ.eyJpc3MiOiJtb2JpbGVjbGllbnRhY2Nlc3Muc3RhZ2UxLm5nLmJsdWVtaXgubmV0IiwiZXhwIjoxNDg3MDg0ODc4LCJhdWQiOiIyNmNiMDEyZWIzMjdjNjEyZDkwYTY4MTkxNjNiNmJjYmQ0ODQ5Y2JiIiwiaWF0IjoxNDg3MDgxMjc4LCJhdXRoX2J5IjoiZmFjZWJvb2siLCJ0ZW5hbnQiOiI0ZGJhOTQzMC01NGU2LTRjZjItYTUxNi02ZjczZmViNzAyYmIiLCJzY29wZSI6ImFwcGlkX2RlZmF1bHQgYXBwaWRfcmVhZHByb2ZpbGUgYXBwaWRfcmVhZHVzZXJhdHRyIGFwcGlkX3dyaXRldXNlcmF0dHIifQ.HHterec250JSDY1965cM2DadBznl2wTKmzKNSnfjpdTAqax9VZvV3EwuFbEnGp9-i6AC-OlsVj7xvbALkdjwG2lZvpQx0M_gRc_3E0NiYuOGVolcm0wEXtbtDUFFqZQAf9BYYOPZ8OintdBiwUGETbH1ZRVtUvt3nalIko1OPE1Q12LvuRlhz5MClNHmvxJcXc7kucxCx4s4UFFy_HJA1gow7HWFqc9-PZf4JMWA-siYqPrdw_zYeBTBzE5co92F6JBEtGLLCjhJVz9eYgLLECXbak3z6hOaY9352Weuj7AgMOWxzw56jKKsiixMtvzrCzLVIcRUG96UJszwPHtPlA", 21 | DEV_PUBLIC_KEYS : [{"kty":"RSA","n":"tmHvKoPklP-f7ZmYxOjf292_VdBr110t2X9_77fgTLiSj82W8jZ-m1bZ_JbZSVVhYtyvT61RXoHY0ooH45IHStDDDh7AHo0qdX12SJMl_BfZ1TC2z7Kv8iYERqO0F0fpoHUri0SfLu9_Hp0nTR2b0T2KPub00-BWyIisFuomDSdNdJa6r2SxdtYfAfr6XKDtT1k4qwioWRfeAd_JY0RzgPhlzpzwhwvkkpugGBColWCMXHqELXuX_03x5NUU39vyx1wzBbgHb4Wa4h-FvqYQYscKcSRqT4maSdFxELAPyLsH5TMlW5sOcUrkM7oifmfMRKFNweRk-9toJ3npLv0kxQ","e":"AQAB","kid":"appId-1504675475000"}], 22 | SERVER_URL : "http://mobileclientaccess.stage1.ng.bluemix.net/", 23 | TENANTID : "4dba9430-54e6-4cf2-a516-6f73feb702bb", 24 | CLIENTID : "21e8b523-eb42-4a34-a057-04ca9445f6ff", 25 | BAD_CLIENTID : "111111111111111111111111111111111111", 26 | ISSUER: "mobileclientaccess.stage1.ng.bluemix.net", 27 | CONFIG_ISSUER: "https://eu-gb.appid.test.cloud.ibm.com/oauth/v4/db8a27c4-b887-4f8d-a89f-f12fb775b311", 28 | CONFIG_ISSUER_BLUEMIX: "https://appid-oauth.stage1.eu-gb.bluemix.net", 29 | TOKEN_ISSUER: "https://eu-gb.appid.test.cloud.ibm.com/oauth/v4/db8a27c4-b887-4f8d-a89f-f12fb775b311", 30 | CONFIG_ISSUER_NO_HTTPS: "appid-oauth.stage1.ng.bluemix.net", 31 | TOKEN_ISSUER_NO_HTTPS: "us-south.appid.test.cloud.ibm.com/oauth/v4/4dba9430-54e6-4cf2-a516-6f73feb702bb" 32 | }; 33 | -------------------------------------------------------------------------------- /test/mocks/public-key-util-mock.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const pemFromModExp = require("rsa-pem-from-mod-exp"); 15 | const constants = require("./constants"); 16 | const Q = require("q"); 17 | 18 | module.exports = { 19 | retrievePublicKeys: function(){ 20 | var deferred = Q.defer(); 21 | deferred.resolve(); 22 | return deferred.promise; 23 | }, 24 | 25 | getPublicKeyPemByKid: function () { 26 | var deferred = Q.defer(); 27 | deferred.resolve(pemFromModExp(constants.DEV_PUBLIC_KEYS[0].n, constants.DEV_PUBLIC_KEYS[0].e)); 28 | return deferred.promise; 29 | } 30 | } -------------------------------------------------------------------------------- /test/mocks/request-mock.js: -------------------------------------------------------------------------------- 1 | const previousAccessToken = "test.previousAccessToken.test"; 2 | 3 | module.exports = function (options, callback) { 4 | if (options.formData && options.formData.grant_type === "refresh_token") { 5 | if (options.formData.refresh_token === "WORKING_REFRESH_TOKEN") { 6 | return callback(null, { 7 | statusCode: 200 8 | }, { 9 | "access_token": "access_token_mock", 10 | "id_token": "id_token_mock", 11 | "refresh_token": "refresh_token_mock" 12 | }); 13 | } 14 | if (options.formData.refresh_token === "INVALID_REFRESH_TOKEN") { 15 | return callback(null, { 16 | statusCode: 401 17 | }, { 18 | error: "invalid_grant", 19 | "error_description": "invalid grant" 20 | }); 21 | } 22 | } 23 | if (options.url.indexOf("generate_code") >= 0) { 24 | if (options.auth.bearer.indexOf("error") >= 0) { 25 | return callback(new Error("STUBBED_ERROR"), { 26 | statusCode: 0 27 | }, null); 28 | } 29 | if (options.auth.bearer.indexOf("statusNot200") >= 0) { 30 | return callback(null, { 31 | statusCode: 400 32 | }, null); 33 | } 34 | return callback(null, { 35 | statusCode: 200 36 | }, "1234"); 37 | } 38 | if (options.url.indexOf("FAIL-PUBLIC-KEY") >= 0 || options.url.indexOf("FAIL_REQUEST") >= 0) { // Used in public-key-util-test 39 | return callback(new Error("STUBBED_ERROR"), { 40 | statusCode: 0 41 | }, null); 42 | } else if (options.url.indexOf("SUCCESS-PUBLIC-KEY") !== -1) { // Used in public-key-util-test 43 | return callback(null, { 44 | statusCode: 200 45 | }, { 46 | "n": 1, 47 | "e": 2 48 | }); 49 | } else if (options.formData && options.formData.code && options.formData.code.indexOf("FAILING_CODE") !== -1) { // Used in webapp-strategy-test 50 | return callback(new Error("STUBBED_ERROR"), { 51 | statusCode: 0 52 | }, null); 53 | } else if (options.formData && options.formData.code && options.formData.code.indexOf("WORKING_CODE") !== -1) { // Used in webapp-strategy-test 54 | return callback(null, { 55 | statusCode: 200 56 | }, { 57 | "access_token": "access_token_mock", 58 | "id_token": "id_token_mock", 59 | "refresh_token": "refresh_token_mock" 60 | }); 61 | } else if (options.followRedirect === false) { 62 | return callback(null, { 63 | statusCode: 302, 64 | headers: { 65 | location: "test-location?code=WORKING_CODE" 66 | } 67 | }); 68 | } else if (options.formData && options.formData.code && options.formData.code.indexOf("NULL_ID_TOKEN") !== -1) { 69 | return callback(null, { 70 | statusCode: 200 71 | }, { 72 | "access_token": "access_token_mock", 73 | "id_token": "null_scope", 74 | "refresh_token": "refresh_token_mock" 75 | }); 76 | } else if (options.formData.username === "test_username" && options.formData.password === "bad_password") { 77 | return callback(null, { 78 | statusCode: 401 79 | }, { 80 | error: "invalid_grant", 81 | "error_description": "wrong credentials" 82 | }); 83 | } else if (options.formData.username === "request_error") { 84 | return callback(new Error("REQUEST_ERROR"), { 85 | statusCode: 0 86 | }, null); 87 | } else if (options.formData.username === "parse_error") { 88 | return callback(null, { 89 | statusCode: 401 90 | }, { 91 | error: "invalid_grant", 92 | "error_description": "wrong credentials" 93 | } + "dddddd"); 94 | } else if (options.formData.username === "test_username" && options.formData.password === "good_password") { 95 | if (options.formData.scope) { 96 | return callback(null, { 97 | statusCode: 200 98 | }, { 99 | "access_token": "access_token_mock_test_scope", 100 | "id_token": "id_token_mock_test_scope", 101 | "refresh_token": "refrehs_token_test_scope" 102 | }); 103 | } 104 | if (options.formData.appid_access_token) { 105 | if (options.formData.appid_access_token === previousAccessToken) { 106 | return callback(null, { 107 | statusCode: 200 108 | }, { 109 | "access_token": "access_token_mock", 110 | "id_token": "id_token_mock", 111 | "previousAccessToken": previousAccessToken, 112 | "refresh_token": "refresh_token_mock" 113 | }); 114 | } 115 | return callback(null, { 116 | statusCode: 400 117 | }, {}); 118 | } 119 | return callback(null, { 120 | statusCode: 200 121 | }, { 122 | "access_token": "access_token_mock", 123 | "id_token": "id_token_mock", 124 | "refresh_token": "refresh_token_mock" 125 | }); 126 | } 127 | 128 | throw "Unhandled case!!!" + JSON.stringify(options); 129 | }; -------------------------------------------------------------------------------- /test/mocks/token-util-mock.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const Q = require("q"); 15 | 16 | function decode(tokenString) { 17 | if (tokenString === "invalid_token") { 18 | return undefined; 19 | } else if (tokenString === "bad_scope") { 20 | return {scope: "bad_scope"}; 21 | } else if (tokenString === "null_scope") { 22 | return null; 23 | } else if (tokenString === "access_token_mock_test_scope") { 24 | return {scope: "test_scope"}; 25 | } else if (tokenString === "id_token_mock_test_scope") { 26 | return {scope: "test_scope"}; 27 | } else { 28 | return {scope: "appid_default"}; 29 | } 30 | } 31 | 32 | function decodeAndValidate(tokenString) { 33 | let deferred = Q.defer(); 34 | if (tokenString === "invalid_token") { 35 | deferred.resolve(); 36 | } else if (tokenString === "bad_scope") { 37 | deferred.resolve({scope: "bad_scope", aud: ["myClientId"]}); 38 | } else if (tokenString === "null_scope") { 39 | deferred.resolve(null); 40 | } else if (tokenString === "access_token_mock_test_scope" || tokenString === "id_token_mock_test_scope") { 41 | deferred.resolve({scope: "test_scope", aud: ["myClientId"]}); 42 | } else if (tokenString === "access_token_3_scopes" || tokenString === "id_token_3_scopes") { 43 | deferred.resolve({scope: "appid_default scope1 scope2 scope3", aud: ["myClientId"]}); 44 | } else { 45 | deferred.resolve({scope: "appid_default" , aud: ["myClientId"]}); 46 | } 47 | return deferred.promise; 48 | } 49 | 50 | let isIssuerAndAudValid = true; 51 | let shouldSwitchIssuerState = false; 52 | const switchIssuerState = () => shouldSwitchIssuerState = true; 53 | const setValidateIssAndAudResponse = (isValid) => isIssuerAndAudValid = isValid; 54 | const checkSwitch = () => { 55 | if (shouldSwitchIssuerState) { 56 | isIssuerAndAudValid = !isIssuerAndAudValid; 57 | shouldSwitchIssuerState = false; 58 | } 59 | }; 60 | 61 | function validateIssAndAud(token, serviceConfig) { 62 | if (isIssuerAndAudValid) { 63 | checkSwitch(); 64 | return Promise.resolve(true); 65 | } else { 66 | checkSwitch(); 67 | return Promise.reject(new Error("no")); 68 | } 69 | } 70 | 71 | function getRandomNumber() { 72 | return "123456789"; 73 | } 74 | 75 | module.exports = { 76 | decodeAndValidate, 77 | decode, 78 | validateIssAndAud, 79 | getRandomNumber, 80 | setValidateIssAndAudResponse, 81 | switchIssuerState 82 | }; 83 | -------------------------------------------------------------------------------- /test/public-key-util-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const chai = require("chai"); 15 | const assert = chai.assert; 16 | const proxyquire = require("proxyquire"); 17 | const testServerUrl = "https://mobileclientaccess.test.url/imf-authserver"; 18 | var requestCounter = 0; 19 | var seqRequestCounter = 0; 20 | const Q = require("q"); 21 | 22 | describe("/lib/utils/public-key-util", function () { 23 | console.log("Loading public-key-util-test.js"); 24 | 25 | var PublicKeyUtil; 26 | 27 | before(function () { 28 | PublicKeyUtil = proxyquire("../lib/utils/public-key-util", { 29 | "../utils/request-util": requestMock 30 | }); 31 | }); 32 | 33 | this.timeout(5000); 34 | 35 | 36 | describe("getPublicKeyPemByKid", function () { 37 | 38 | it("public key dont have kid value", function (done) { 39 | var kid; 40 | PublicKeyUtil.getPublicKeyPemByKid(kid).then(function (publicKey) { 41 | done("should get to catch"); 42 | }).catch(function (err) { 43 | assert.equal(err, "Passed token does not have kid value."); 44 | done(); 45 | }) 46 | }); 47 | 48 | it("request to public keys endpoint failure", function (done) { 49 | var kid = "not_found_kid"; 50 | PublicKeyUtil.getPublicKeyPemByKid(kid, testServerUrl + "FAIL-PUBLIC-KEYs").then(function () { 51 | done("should get reject"); 52 | }).catch(function (err) { 53 | try { 54 | assert.equal(err, "updatePublicKeys error: Failed to retrieve public keys. All requests to protected endpoints will be rejected."); 55 | done(); 56 | } catch (e) { 57 | done(e); 58 | } 59 | }); 60 | }); 61 | 62 | it("request to public keys endpoint update failure", function (done) { 63 | var kid = "123"; 64 | PublicKeyUtil.getPublicKeyPemByKid(kid, testServerUrl + "SUCCESS-PUBLIC-KEYs").then(function () { 65 | kid = "not_found_kid"; 66 | PublicKeyUtil.getPublicKeyPemByKid(kid, testServerUrl + "FAIL-PUBLIC-KEYs").then(function () { 67 | done("should get reject"); 68 | }).catch(function (err) { 69 | try { 70 | assert.equal(err, "updatePublicKeys error: Failed to retrieve public keys. All requests to protected endpoints will be rejected."); 71 | done(); 72 | } catch (e) { 73 | done(e); 74 | } 75 | }); 76 | }).catch(function (err) { 77 | done(err); 78 | }); 79 | }); 80 | 81 | it("two sequential request to public keys endpoint", function (done) { 82 | var PublicKeyUtilNew = proxyquire("../lib/utils/public-key-util", { 83 | "../utils/request-util": requestMock 84 | }); 85 | var kid = "123"; 86 | PublicKeyUtilNew.getPublicKeyPemByKid(kid, testServerUrl + "SEQUENTIAL-REQUEST-PUBLIC-KEYs").then(function () { 87 | PublicKeyUtilNew.getPublicKeyPemByKid(kid, testServerUrl + "SEQUENTIAL-REQUEST-PUBLIC-KEYs").then(function () { 88 | assert.equal(1, seqRequestCounter, "more then one request triggered"); 89 | done(); 90 | }).catch(function (err) { 91 | done(err); 92 | }); 93 | }).catch(function (err) { 94 | done(err); 95 | }); 96 | }); 97 | 98 | it("Should successfully retrieve public key from OAuth server", function (done) { 99 | var kid = "123"; 100 | PublicKeyUtil.getPublicKeyPemByKid(kid, testServerUrl + "SUCCESS-PUBLIC-KEYs").then(function (publicKey) { 101 | try { 102 | assert.isNotNull(publicKey); 103 | assert.isString(publicKey); 104 | assert.include(publicKey, "BEGIN RSA PUBLIC KEY"); 105 | done(); 106 | } catch (e) { 107 | done(e); 108 | } 109 | }).catch(function (err) { 110 | done(err); 111 | }); 112 | }); 113 | }); 114 | 115 | describe("getPublicKeyPemMultipleRequests", function () { 116 | it("Should get public keys from multiple requests", function (done) { 117 | var PublicKeyUtilNew = proxyquire("../lib/utils/public-key-util", { 118 | "../utils/request-util": requestMock 119 | }); 120 | var requestArray = []; 121 | for (var i = 0; i < 5; i++) { 122 | requestArray.push(PublicKeyUtilNew.getPublicKeyPemByKid("123", testServerUrl + "SETTIMEOUT-PUBLIC-KEYs")); 123 | } 124 | Q.all(requestArray).then(function (publicKeysArray) { 125 | try { 126 | assert.equal(1, requestCounter, "more then one request triggered"); 127 | for (var j = 0; j < 5; j++) { 128 | assert.isNotNull(publicKeysArray[j]); 129 | assert.isString(publicKeysArray[j]); 130 | assert.include(publicKeysArray[j], "BEGIN RSA PUBLIC KEY"); 131 | } 132 | done(); 133 | } catch (e) { 134 | done(e); 135 | } 136 | }).catch(function (err) { 137 | done(err); 138 | }); 139 | 140 | 141 | }); 142 | }); 143 | }); 144 | 145 | var requestMock = function (options, callback) { 146 | if (options.url.indexOf("FAIL-PUBLIC-KEY") >= 0 || options.url.indexOf("FAIL_REQUEST") >= 0) { // Used in public-key-util-test 147 | return callback(new Error("STUBBED_ERROR"), { 148 | statusCode: 0 149 | }, null); 150 | } else if (options.url.indexOf("SUCCESS-PUBLIC-KEY") !== -1) { // Used in public-key-util-test 151 | return callback(null, { 152 | statusCode: 200 153 | }, { 154 | "keys": [{ 155 | "n": "1", 156 | "e": "2", 157 | "kid": "123" 158 | }] 159 | }); 160 | } else if (options.formData && options.formData.code && options.formData.code.indexOf("FAILING_CODE") !== -1) { // Used in webapp-strategy-test 161 | return callback(new Error("STUBBED_ERROR"), { 162 | statusCode: 0 163 | }, null); 164 | } else if (options.formData && options.formData.code && options.formData.code.indexOf("WORKING_CODE") !== -1) { // Used in webapp-strategy-test 165 | return callback(null, { 166 | statusCode: 200 167 | }, JSON.stringify({ 168 | "access_token": "access_token_mock", 169 | "id_token": "id_token_mock" 170 | })); 171 | } else if (options.followRedirect === false) { 172 | return callback(null, { 173 | statusCode: 302, 174 | headers: { 175 | location: "test-location?code=WORKING_CODE" 176 | } 177 | }); 178 | } else if (options.url.indexOf("SETTIMEOUT-PUBLIC-KEYs") > -1) { 179 | requestCounter++; 180 | setTimeout(function () { 181 | return callback(null, { 182 | statusCode: 200 183 | }, { 184 | "keys": [{ 185 | "n": "1", 186 | "e": "2", 187 | "kid": "123" 188 | }] 189 | }); 190 | }, 3000); 191 | } else if (options.url.indexOf("SEQUENTIAL-REQUEST-PUBLIC-KEYs") > -1) { 192 | seqRequestCounter++; 193 | return callback(null, { 194 | statusCode: 200 195 | }, { 196 | "keys": [{ 197 | "n": "1", 198 | "e": "2", 199 | "kid": "123" 200 | }] 201 | }); 202 | } else { 203 | throw "Unhandled case!!!" + JSON.stringify(options); 204 | } 205 | }; -------------------------------------------------------------------------------- /test/request-util-test.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon'); 2 | const proxyquire = require('proxyquire').noPreserveCache(); 3 | const chai = require("chai"); 4 | const expect = chai.expect; 5 | const { 6 | jsonToURLencodedForm, 7 | parseFormData 8 | } = require('../lib/utils/common-util'); 9 | const sinonChai = require('sinon-chai'); 10 | var sandbox = sinon.createSandbox(); 11 | chai.use(sinonChai); 12 | 13 | describe('/lib/utils/request-util', function (done) { 14 | let requestUtil; 15 | 16 | let gotStub = sandbox.stub().resolves({ 17 | body: { 18 | "name": "Abod", 19 | "car": null 20 | } 21 | }); 22 | 23 | before(function () { 24 | requestUtil = proxyquire("../lib/utils/request-util", { 25 | got: gotStub, 26 | }); 27 | }); 28 | 29 | afterEach(() => { 30 | gotStub.resetHistory(); 31 | sandbox.restore(); 32 | }) 33 | 34 | context('Insure the headers option are updated to match Got library configuration', () => { 35 | 36 | it('should add a content-type header (application/json) if nothing specified for Non Get methods', (done) => { 37 | const reqHeaders = { 38 | url: 'sampleURL', 39 | method: "POST" 40 | } 41 | 42 | const expectedHeaders = { 43 | headers: { 44 | "content-type": "application/json" 45 | } 46 | } 47 | 48 | const callbackFun = (error, response, body) => { 49 | expect(gotStub).to.have.been.calledOnce; 50 | expect(gotStub).to.have.been.deep.calledWithMatch('sampleURL', expectedHeaders); 51 | done(); 52 | } 53 | requestUtil(reqHeaders, callbackFun); 54 | }); 55 | 56 | 57 | it('should NOT override the content-type header if one was specified for Non Get methods', (done) => { 58 | const reqHeaders = { 59 | url: 'sampleURL', 60 | method: "POST", 61 | headers: { 62 | "Content-Type": "application/x-www-form-urlencoded" 63 | } 64 | } 65 | 66 | const expectedHeaders = { 67 | headers: { 68 | "content-type": "application/x-www-form-urlencoded" 69 | } 70 | } 71 | 72 | const callbackFun = (error, response, body) => { 73 | expect(gotStub).to.have.been.calledOnce; 74 | expect(gotStub).to.have.been.deep.calledWithMatch('sampleURL', expectedHeaders); 75 | done(); 76 | } 77 | requestUtil(reqHeaders, callbackFun); 78 | }); 79 | 80 | it('should replace the (qs) option header with (searchParams)', (done) => { 81 | const reqHeaders = { 82 | url: 'sampleURL', 83 | qs: { 84 | r: "r" 85 | }, 86 | method: "GET" 87 | } 88 | 89 | const expectedHeaders = { 90 | searchParams: { 91 | r: "r" 92 | }, 93 | method: "GET" 94 | } 95 | 96 | const callbackFun = (error, response, body) => { 97 | expect(gotStub).to.have.been.calledOnce; 98 | expect(gotStub).to.have.been.deep.calledWithMatch('sampleURL', expectedHeaders); 99 | done(); 100 | } 101 | requestUtil(reqHeaders, callbackFun); 102 | }); 103 | }); 104 | 105 | context('should replace the (form) option with the corresponding body type based on the content-type header', () => { 106 | 107 | it('should replace the (form) option with JSON.stringify Body', (done) => { 108 | const reqHeaders = { 109 | url: 'sampleURL', 110 | form: { 111 | r: "r" 112 | } 113 | } 114 | 115 | const expectedHeaders = { 116 | body: '{"r":"r"}', 117 | headers: { 118 | "content-type": "application/json" 119 | } 120 | } 121 | 122 | const callbackFun = (error, response, body) => { 123 | expect(gotStub).to.have.been.calledOnce; 124 | expect(gotStub).to.have.been.deep.calledWithMatch('sampleURL', expectedHeaders); 125 | done(); 126 | } 127 | requestUtil(reqHeaders, callbackFun); 128 | }); 129 | 130 | 131 | it('should replace the (form) option with URLencodedForm Body if content-type is application/x-www-form-urlencoded ', (done) => { 132 | const sampleForm = { 133 | r: "r", 134 | s: "sss" 135 | }; 136 | const reqHeaders = { 137 | url: 'sampleURL', 138 | form: sampleForm, 139 | headers: { 140 | "content-type": "application/x-www-form-urlencoded" 141 | } 142 | } 143 | 144 | const expectedHeaders = { 145 | body: jsonToURLencodedForm(sampleForm), 146 | headers: { 147 | "content-type": "application/x-www-form-urlencoded" 148 | } 149 | } 150 | 151 | const callbackFun = (error, response, body) => { 152 | expect(gotStub).to.have.been.calledOnce; 153 | expect(gotStub).to.have.been.deep.calledWithMatch('sampleURL', expectedHeaders); 154 | done(); 155 | } 156 | requestUtil(reqHeaders, callbackFun); 157 | }); 158 | }); 159 | 160 | context('should replace the (auth) option with the corresponding Authorization header', () => { 161 | 162 | it('should replace the (auth) for Bearer Authorization', (done) => { 163 | const sampleToken = "sampleToken"; 164 | const reqHeaders = { 165 | url: 'sampleURL', 166 | auth: { 167 | bearer: sampleToken 168 | } 169 | } 170 | 171 | const expectedHeaders = { 172 | headers: { 173 | "Authorization": `Bearer ${sampleToken}`, 174 | "content-type": "application/json" 175 | } 176 | } 177 | 178 | const callbackFun = (error, response, body) => { 179 | expect(gotStub).to.have.been.calledOnce; 180 | expect(gotStub).to.have.been.deep.calledWithMatch('sampleURL', expectedHeaders); 181 | done(); 182 | } 183 | requestUtil(reqHeaders, callbackFun); 184 | }); 185 | 186 | 187 | it('should replace the (auth) for Basic Authorization', (done) => { 188 | const authUsername = "sampleAuthUsername"; 189 | const authPassword = "sampleAuthPassword"; 190 | const reqHeaders = { 191 | url: 'sampleURL', 192 | auth: { 193 | username: authUsername, 194 | password: authPassword, 195 | } 196 | } 197 | 198 | const expectedHeaders = { 199 | headers: { 200 | "Authorization": "Basic " + Buffer.from(`${authUsername}:${authPassword}`).toString("base64"), 201 | "content-type": "application/json" 202 | } 203 | } 204 | 205 | const callbackFun = (error, response, body) => { 206 | expect(gotStub).to.have.been.calledOnce; 207 | expect(gotStub).to.have.been.deep.calledWithMatch('sampleURL', expectedHeaders); 208 | done(); 209 | } 210 | requestUtil(reqHeaders, callbackFun); 211 | }); 212 | }); 213 | 214 | context('should replace the formData and json option with the corresponding body option', () => { 215 | const sampleForm = { 216 | firstName: 'Abod', 217 | lastName: 'Akhras', 218 | }; 219 | 220 | const expectedHeaders = { 221 | body: JSON.stringify(sampleForm), 222 | headers: { 223 | "content-type": "application/json" 224 | } 225 | } 226 | 227 | it('should replace the (formData) with stringified JSON object', (done) => { 228 | const reqHeaders = { 229 | url: 'sampleURL', 230 | formData: sampleForm 231 | } 232 | 233 | 234 | const callbackFun = (error, response, body) => { 235 | const bodyParamPassed = gotStub.args[0][1].body; 236 | 237 | expect(gotStub).to.have.been.calledOnce; 238 | 239 | // Expect the body to be passed as a stream 240 | expect(bodyParamPassed).to.have.property('_streams'); 241 | expect(parseFormData(bodyParamPassed)).to.deep.equal(sampleForm); 242 | done(); 243 | } 244 | requestUtil(reqHeaders, callbackFun); 245 | }); 246 | 247 | it('should replace the (json) with stringified JSON object', (done) => { 248 | const reqHeaders = { 249 | url: 'sampleURL', 250 | json: sampleForm 251 | } 252 | 253 | const callbackFun = (error, response, body) => { 254 | expect(gotStub).to.have.been.calledOnce; 255 | expect(gotStub).to.have.been.deep.calledWithMatch('sampleURL', expectedHeaders); 256 | done(); 257 | } 258 | requestUtil(reqHeaders, callbackFun); 259 | }); 260 | }); 261 | 262 | 263 | context('Handle error failures', () => { 264 | const reqHeaders = { 265 | url: 'sampleURL', 266 | json: { 267 | val: 'SomeVal' 268 | } 269 | } 270 | 271 | beforeEach(function () { 272 | sandbox.reset(); 273 | }); 274 | 275 | it('should return an error if body has error value', (done) => { 276 | const someError = "someError"; 277 | gotStub = sandbox.stub().resolves({ 278 | "error": someError 279 | }); 280 | requestUtil = proxyquire("../lib/utils/request-util", { 281 | got: gotStub, 282 | }); 283 | 284 | const callbackFun = (error, response, body) => { 285 | expect(gotStub).to.have.been.calledOnce; 286 | expect(error).to.equal(someError); 287 | done(); 288 | } 289 | requestUtil(reqHeaders, callbackFun); 290 | 291 | }); 292 | 293 | it('should return an error if body was not sent back2', (done) => { 294 | let sampleError = new Error("Some Error"); 295 | sampleError.response = {}; 296 | sampleError.response.statusCode = 500; 297 | gotStub = sandbox.stub().rejects(sampleError); 298 | requestUtil = proxyquire("../lib/utils/request-util", { 299 | got: gotStub, 300 | }); 301 | 302 | const callbackFun = (error, response, body) => { 303 | expect(gotStub).to.have.been.calledOnce; 304 | expect(error).to.equal(sampleError); 305 | expect(response).to.deep.equal({ 306 | statusCode: sampleError.response.statusCode 307 | }); 308 | done(); 309 | } 310 | requestUtil(reqHeaders, callbackFun); 311 | }); 312 | }); 313 | 314 | }); -------------------------------------------------------------------------------- /test/token-manager-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 IBM Corp. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | const chai = require("chai"); 15 | const assert = chai.assert; 16 | const proxyquire = require("proxyquire"); 17 | 18 | const mockJwsToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Qta2lkIn0.eyJpc3MiOiJ0ZXN0LWN1c3RvbS1pZGVudGl0eSIsInN1YiI6InRlc3QtdW5pcXVlLXVzZXItaWQiLCJhdWQiOiJ0ZXN0LWFwcGlkLW9hdXRoLmJsdWVtaXgubmV0IiwiZXhwIjo5OTk5OTk5OTk5LCJuYW1lIjoidGVzdC11c2VyLW5hbWUiLCJlbWFpbCI6InRlc3QtdXNlci1lbWFpbCIsImdlbmRlciI6InRlc3QtdXNlci1nZW5kZXIiLCJwaWN0dXJlIjoidGVzdC11c2VyLXBpY3R1cmUiLCJsb2NhbGUiOiJ0ZXN0LXVzZXItbG9jYWxlIiwiZ3JvdXAiOiJjdXN0b20gaWRwIGdyb3VwIiwic2NvcGUiOiJjdXN0b21TY29wZSIsImlhdCI6OTk5OTk5OTk5OX0.NUCyUyxxfhVVmuYYW2atBKmo9anqBEIFV3IPNShPKI6Ssl9t-Wx0DlKG-bGxr5d6tABoWFgE_7tat0Y6F-LEEcLeLeJSCyEU9PC245xNnyRlbKaZtGOj3ii_n6AV9AW-fKuTiPMXfaqMWyudyxCXVH_J5mubegAyelwxA0VxfeY'; 19 | 20 | const mockAccessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJ0ZXN0LWtpZCJ9.eyJpc3MiOiJ0ZXN0LWFwcGlkLW9hdXRoLmJsdWVtaXgubmV0IiwiZXhwIjo5OTk5OTk5OTk5LCJhdWQiOiJ0ZXN0LWNsaWVudC1pZCIsInN1YiI6InRlc3Qtc3ViIiwiYW1yIjpbImFwcGlkX2N1c3RvbSJdLCJpYXQiOjk5OTk5OTk5OTksInRlbmFudCI6InRlc3QtdGVuYW50LWlkIiwic2NvcGUiOiJvcGVuaWQgYXBwaWRfZGVmYXVsdCBhcHBpZF9yZWFkcHJvZmlsZSBhcHBpZF9yZWFkdXNlcmF0dHIgYXBwaWRfd3JpdGV1c2VyYXR0ciBhcHBpZF9hdXRoZW50aWNhdGVkIGN1c3RvbVNjb3BlIn0.SGNED1HBVjdqc7NEwsjOIwtIlhgNseVE6QzRrEAuRaej4RHhq1Fxyhc-r__1qdCFI2ZmUx02jSGvg2lyj7f1nm8ax9CbjW3TpOGJQjA-EPX8PUnbG-sTeusO24PEZP04Qcquga9t-dON3Uy20sDNk3WcDvb_dxtYo6NPco5Y0bQ'; 21 | 22 | const mockIdentityToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJ0ZXN0LWtpZCJ9.eyJpc3MiOiJ0ZXN0LWFwcGlkLW9hdXRoLmJsdWVtaXgubmV0IiwiYXVkIjoidGVzdC1jbGllbnQtaWQiLCJleHAiOjk5OTk5OTk5OTksInRlbmFudCI6InRlc3QtdGVuYW50LWlkIiwiaWF0Ijo5OTk5OTk5OTk5LCJlbWFpbCI6InRlc3QtdXNlci1lbWFpbCIsIm5hbWUiOiJ0ZXN0LXVzZXItbmFtZSIsImdlbmRlciI6InRlc3QtdXNlci1nZW5kZXIiLCJsb2NhbGUiOiJ0ZXN0LXVzZXItbG9jYWxlIiwicGljdHVyZSI6InRlc3QtdXNlci1waWN0dXJlIiwic3ViIjoidGVzdC1zdWIiLCJpZGVudGl0aWVzIjpbeyJwcm92aWRlciI6ImFwcGlkX2N1c3RvbSIsImlkIjoidGVzdC11bmlxdWUtdXNlci1pZCJ9XSwiYW1yIjpbImFwcGlkX2N1c3RvbSJdfQ.LX627K3x69Z_BeMw1iQ5aFJf2PjRte-wa81wdmW47VuleahhYRTmXguGxhwad3GTTtDfwhtL0muxAVgywgyyQ5c3gz1pSZ-k-b2M6vu39Owap3pb7NZXNoqA34us17E4zfqXSVzXMWEwfnhlX5bQZdCpUVypmGwsG4ng94f26m4'; 23 | 24 | const mockTokenResponse = { 25 | access_token: mockAccessToken, 26 | id_token: mockIdentityToken, 27 | token_type: 'Bearer', 28 | expires_in: 9999999999 29 | }; 30 | 31 | const SUCCESS = 'success'; 32 | const INVALID_ACCESS_TOKEN = 'invalid_access_token'; 33 | const INVALID_IDENTITY_TOKEN = 'invalid_identity_token'; 34 | const BAD_REQUEST = 'return_code:400'; 35 | const UNAUTHORIZED = 'return_code:401'; 36 | const NOT_FOUND = 'return_code:404'; 37 | const SERVER_ERROR = 'return_code:500'; 38 | 39 | const CUSTOM = 'CUSTOM'; 40 | const APP_TO_APP = 'APP2APP'; 41 | 42 | const mockConfig = (event) => ({ 43 | tenantId: 'test-tenant-id', 44 | clientId: 'test-client-id', 45 | secret: `secret ${event}`, 46 | oauthServerUrl: 'https://test-appid-oauth.bluemix.net/oauth/v3/test-tenant-id' 47 | }); 48 | 49 | function getErrorResponse(statusCode) { 50 | let errorResponse = { 51 | statusCode 52 | }; 53 | if (statusCode === 400) { 54 | errorResponse['error_description'] = 'Bad request'; 55 | } else if (statusCode === 401) { 56 | errorResponse['error_description'] = 'Unauthorized'; 57 | } else if (statusCode === 404) { 58 | errorResponse['error_description'] = 'Not Found'; 59 | } else { 60 | errorResponse['error_description'] = 'Unexpected error'; 61 | } 62 | return errorResponse; 63 | } 64 | 65 | function mockRequest(options, callback) { 66 | const secret = options.auth.password; 67 | if (secret.includes(INVALID_ACCESS_TOKEN)) { 68 | const mockInvalidTokenResponse = Object.create(mockTokenResponse); 69 | mockInvalidTokenResponse['access_token'] = 'invalid_token'; 70 | return callback(null, { 71 | statusCode: 200 72 | }, mockInvalidTokenResponse); 73 | } else if (secret.includes(INVALID_IDENTITY_TOKEN)) { 74 | const mockInvalidTokenResponse = Object.create(mockTokenResponse); 75 | mockInvalidTokenResponse['id_token'] = 'invalid_token'; 76 | return callback(null, { 77 | statusCode: 200 78 | }, mockInvalidTokenResponse); 79 | } else if (secret.includes(SUCCESS)) { 80 | return callback(null, { 81 | statusCode: 200 82 | }, mockTokenResponse) 83 | } else if (secret.includes('return_code')) { 84 | const statusCode = parseInt(secret.split(':')[1]); 85 | return callback(null, { 86 | statusCode 87 | }, getErrorResponse(statusCode)); 88 | } else if (secret.includes('ERROR')) { 89 | return callback(new Error('Error'), { 90 | statusCode: 500 91 | }, ""); 92 | } 93 | } 94 | 95 | function mockRetrieveTokenFailure(tokenManager, grantType, expectedErrMessage, done) { 96 | 97 | let params = []; 98 | let funcToTest; 99 | switch (grantType) { 100 | case CUSTOM: { 101 | params.push(mockJwsToken); 102 | funcToTest = tokenManager.getCustomIdentityTokens; 103 | break; 104 | } 105 | 106 | case APP_TO_APP: { 107 | funcToTest = tokenManager.getApplicationIdentityToken; 108 | break; 109 | } 110 | 111 | default: { 112 | throw Error('Invalid function to test'); 113 | } 114 | 115 | } 116 | 117 | funcToTest.apply(tokenManager, params) 118 | .catch((error) => { 119 | assert.equal(error.message, expectedErrMessage); 120 | done(); 121 | }); 122 | 123 | } 124 | 125 | 126 | describe('/lib/token-manager/token-manager', () => { 127 | let TokenManager; 128 | 129 | before(() => { 130 | TokenManager = proxyquire("../lib/token-manager/token-manager", { 131 | "../utils/token-util": require("./mocks/token-util-mock"), 132 | "../utils/request-util": mockRequest 133 | }); 134 | }); 135 | 136 | describe('#TokenManager.getCustomIdentityTokens', () => { 137 | it('Should fail access token validation', function (done) { 138 | const tokenManager = new TokenManager(mockConfig(INVALID_ACCESS_TOKEN)); 139 | mockRetrieveTokenFailure(tokenManager, CUSTOM, 'Invalid access token', done); 140 | }); 141 | 142 | it('Should fail for thrown error', function (done) { 143 | const tokenManager = new TokenManager(mockConfig('ERROR')); 144 | mockRetrieveTokenFailure(tokenManager, CUSTOM, 'Error', done); 145 | }); 146 | 147 | it('Should fail identity token validation', function (done) { 148 | const tokenManager = new TokenManager(mockConfig(INVALID_IDENTITY_TOKEN)); 149 | mockRetrieveTokenFailure(tokenManager, CUSTOM, 'Invalid identity token', done); 150 | }); 151 | 152 | it('Should fail to retrieve tokens - 400', function (done) { 153 | const tokenManager = new TokenManager(mockConfig(BAD_REQUEST)); 154 | mockRetrieveTokenFailure(tokenManager, CUSTOM, 'Failed to obtain tokens', done); 155 | }); 156 | 157 | it('Should not retrieve tokens - 401', function (done) { 158 | const tokenManager = new TokenManager(mockConfig(UNAUTHORIZED)); 159 | mockRetrieveTokenFailure(tokenManager, CUSTOM, 'Unauthorized', done); 160 | }); 161 | 162 | it('Should not retrieve tokens - 404', function (done) { 163 | const tokenManager = new TokenManager(mockConfig(NOT_FOUND)); 164 | mockRetrieveTokenFailure(tokenManager, CUSTOM, 'Not found', done); 165 | }); 166 | 167 | it('Should not retrieve tokens - 500', function (done) { 168 | const tokenManager = new TokenManager(mockConfig(SERVER_ERROR)); 169 | mockRetrieveTokenFailure(tokenManager, CUSTOM, 'Unexpected error', done); 170 | }); 171 | 172 | it('Should retrieve tokens - Happy Flow', (done) => { 173 | const tokenManager = new TokenManager(mockConfig(SUCCESS)); 174 | tokenManager.getCustomIdentityTokens(mockJwsToken) 175 | .then((context) => { 176 | assert.equal(context.accessToken, mockAccessToken); 177 | assert.equal(context.identityToken, mockIdentityToken); 178 | assert.equal(context.expiresIn, 9999999999); 179 | assert.equal(context.tokenType, 'Bearer'); 180 | done(); 181 | }) 182 | .catch((error) => done(error)); 183 | }); 184 | }); 185 | 186 | 187 | describe('#TokenManager.getAppToAppToken', () => { 188 | 189 | it('Should fail token validation - wrong tenant', function (done) { 190 | const tokenManager = new TokenManager(mockConfig(INVALID_ACCESS_TOKEN)); 191 | mockRetrieveTokenFailure(tokenManager, APP_TO_APP, 'Invalid access token', done); 192 | }); 193 | 194 | it('Should not retrieve tokens - 404', function (done) { 195 | const tokenManager = new TokenManager(mockConfig(NOT_FOUND)); 196 | mockRetrieveTokenFailure(tokenManager, APP_TO_APP, 'Not found', done); 197 | 198 | }); 199 | 200 | it('Should not retrieve tokens - 401', function (done) { 201 | const tokenManager = new TokenManager(mockConfig(UNAUTHORIZED)); 202 | mockRetrieveTokenFailure(tokenManager, APP_TO_APP, 'Unauthorized', done) 203 | }); 204 | 205 | it('Should not retrieve tokens - 400', function (done) { 206 | const tokenManager = new TokenManager(mockConfig(BAD_REQUEST)); 207 | mockRetrieveTokenFailure(tokenManager, APP_TO_APP, 'Failed to obtain tokens', done); 208 | }); 209 | 210 | it('Should not retrieve tokens - 500', function (done) { 211 | const tokenManager = new TokenManager(mockConfig(SERVER_ERROR)); 212 | mockRetrieveTokenFailure(tokenManager, APP_TO_APP, 'Unexpected error', done); 213 | }); 214 | 215 | it('should retrieve tokens - Happy Flow', function (done) { 216 | 217 | const tokenManager = new TokenManager(mockConfig(SUCCESS)); 218 | tokenManager.getApplicationIdentityToken().then((context) => { 219 | assert.equal(context.accessToken, mockAccessToken) 220 | assert.equal(context.expiresIn, 9999999999) 221 | assert.equal(context.tokenType, 'Bearer') 222 | done(); 223 | }).catch((err) => { 224 | done(err); 225 | }); 226 | }); 227 | }); 228 | 229 | }); --------------------------------------------------------------------------------