├── .eslintrc.js ├── .github ├── FUNDING.yml ├── codeql │ └── codeql-config.yml ├── renovate.json └── workflows │ ├── codeql-analysis.yml │ └── main.yml ├── .gitignore ├── .mocharc.yml ├── .npmignore ├── .nycrc ├── LICENSE.md ├── README.md ├── SECURITY.md ├── e2e ├── TestApp.ts ├── playwright │ └── chrome-authenticator-extension.spec.ts ├── protractor │ ├── chrome-authenticator-extension.spec.ts │ └── protractor.conf.js ├── puppeteer │ └── chrome-authenticator-extension.spec.ts └── webdriverio │ ├── chrome-authenticator-extension.spec.ts │ └── wdio.conf.ts ├── extension ├── authenticator.mustache.js └── manifest.mustache.json ├── package-lock.json ├── package.json ├── spec └── Authenticator.spec.ts ├── src ├── Authenticator.ts └── index.ts ├── tsconfig.eslint.json ├── tsconfig.json └── tslint.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaVersion: 6, 6 | sourceType: 'module', 7 | }, 8 | plugins: [ 9 | '@typescript-eslint', 10 | 'simple-import-sort', 11 | ], 12 | extends: [ 13 | 'eslint:recommended', 14 | 'plugin:@typescript-eslint/eslint-recommended', 15 | 'plugin:@typescript-eslint/recommended', 16 | 'plugin:unicorn/recommended', 17 | ], 18 | rules: { 19 | 'indent': 'off', 20 | '@typescript-eslint/indent': ['error', 4], 21 | 22 | 'quotes': 'off', 23 | '@typescript-eslint/quotes': ['error', 'single', { 'allowTemplateLiterals': true }], 24 | 25 | 'simple-import-sort/imports': 'error', 26 | 27 | 'unicorn/empty-brace-spaces': 'off', 28 | 29 | 'unicorn/filename-case': [ 'error', { 30 | 'cases': { 31 | 'pascalCase': true, 32 | 'kebabCase': true 33 | } 34 | }], 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ jan-molak ] 4 | tidelift: "npm/authenticator-browser-extension" 5 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL config" 2 | 3 | disable-default-queries: false 4 | 5 | paths-ignore: 6 | - node_modules 7 | paths: 8 | - src 9 | - extension 10 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "rangeStrategy": "bump", 6 | "packageRules": [ 7 | { 8 | "packagePatterns": ["^@serenity-js"], 9 | "groupName": "Serenity/JS", 10 | "automerge": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ master ] 9 | schedule: 10 | - cron: '41 20 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | language: [ 'javascript' ] 21 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 22 | # Learn more... 23 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | - name: Use Node.js 14.x 29 | uses: actions/setup-node@v2 30 | with: 31 | node-version: 14.x 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v1 36 | with: 37 | config-file: ./.github/codeql/codeql-config.yml 38 | languages: ${{ matrix.language }} 39 | 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 44 | 45 | - name: Verify 46 | run: | 47 | npm ci 48 | npm run lint 49 | npm run compile 50 | 51 | - name: Perform CodeQL Analysis 52 | uses: github/codeql-action/analyze@v1 53 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | types: [ opened, synchronize ] 9 | 10 | jobs: 11 | verify: 12 | runs-on: ubuntu-latest 13 | if: "!contains(github.event.head_commit.message, 'ci skip')" 14 | 15 | strategy: 16 | matrix: 17 | node-version: [ 12.x, 14.x, 16.x ] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Setup firefox 22 | uses: browser-actions/setup-firefox@latest 23 | with: 24 | firefox-version: latest-devedition 25 | - run: firefox --version 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | - name: Setup Node Modules 31 | uses: bahmutov/npm-install@v1 32 | env: 33 | CHROMEDRIVER_FILEPATH: "/usr/bin/chromedriver" 34 | - run: xvfb-run npm run verify 35 | env: 36 | CI: true 37 | - uses: actions/upload-artifact@v2 38 | if: matrix.node-version == '14.x' 39 | with: 40 | name: artifacts 41 | path: | 42 | lib 43 | .nyc_output 44 | reports 45 | 46 | release: 47 | needs: [ verify ] 48 | runs-on: ubuntu-latest 49 | if: github.ref == 'refs/heads/master' 50 | steps: 51 | - uses: actions/checkout@v2 52 | - name: Use Node.js 14.x 53 | uses: actions/setup-node@v2 54 | with: 55 | node-version: 14.x 56 | - name: Setup Node Modules 57 | uses: bahmutov/npm-install@v1 58 | env: 59 | CHROMEDRIVER_FILEPATH: "/usr/bin/chromedriver" 60 | - uses: actions/download-artifact@v2 61 | with: 62 | name: artifacts 63 | - run: npm run publish:reports 64 | env: 65 | COVERALLS_REPO_TOKEN: ${{secrets.COVERALLS_REPO_TOKEN}} 66 | - run: npx semantic-release 67 | env: 68 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 69 | GH_TOKEN: ${{secrets.GH_TOKEN}} 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ 2 | .idea 3 | *.iml 4 | 5 | # Node 6 | node_modules 7 | 8 | # Build process 9 | .nyc_output 10 | build 11 | reports 12 | lib 13 | *.log 14 | 15 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | check-leaks: false 2 | color: true 3 | diff: true 4 | full-trace: true 5 | reporter: dot 6 | require: 'ts-node/register' 7 | timeout: 10000 8 | v8-stack-trace-limit: 100 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | *.log 4 | .npmignore 5 | package-lock.json 6 | 7 | # Supporting files 8 | .nyc_output 9 | spec 10 | e2e 11 | reports 12 | build 13 | 14 | # Config 15 | .travis.yml 16 | tsconfig*.json 17 | tslint.json 18 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/*.ts", 4 | "src/**/*.ts" 5 | ], 6 | "exclude": [ 7 | "lib", 8 | "node_modules", 9 | "spec", 10 | "src/types" 11 | ], 12 | "extension": [ 13 | ".ts" 14 | ], 15 | "require": [ 16 | "ts-node/register" 17 | ], 18 | "reporter": [ 19 | "json", 20 | "text-summary", 21 | "html" 22 | ], 23 | "cache": true, 24 | "all": true 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Authenticator (Browser Extension) 2 | 3 | [![npm version](https://badge.fury.io/js/authenticator-browser-extension.svg)](https://badge.fury.io/js/authenticator-browser-extension) 4 | [![Build Status](https://github.com/jan-molak/authenticator-browser-extension/workflows/build/badge.svg)](https://github.com/jan-molak/authenticator-browser-extension/actions) 5 | [![Coverage Status](https://coveralls.io/repos/github/jan-molak/authenticator-browser-extension/badge.svg)](https://coveralls.io/github/jan-molak/authenticator-browser-extension) 6 | [![npm](https://img.shields.io/npm/dm/authenticator-browser-extension.svg)](https://npm-stat.com/charts.html?package=authenticator-browser-extension) 7 | [![Known Vulnerabilities](https://snyk.io/test/github/jan-molak/authenticator-browser-extension/badge.svg)](https://snyk.io/test/github/jan-molak/authenticator-browser-extension) 8 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjan-molak%2Fauthenticator-browser-extension.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjan-molak%2Fauthenticator-browser-extension?ref=badge_shield) 9 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 10 | 11 | [![Twitter Follow](https://img.shields.io/twitter/follow/JanMolak?style=social)](https://twitter.com/JanMolak) 12 | 13 | Authenticator is a [web browser extension](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions) 14 | that enables your automated tests to authenticate with web apps using [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). 15 | 16 | Authenticator generates the browser extension dynamically, so you can easily provide the username and password 17 | via a config file or env variables. 18 | 19 | Authenticator is [proven to work](https://github.com/jan-molak/authenticator-browser-extension/tree/master/e2e) with following test frameworks: 20 | - [WebdriverIO](https://webdriver.io/) ([example](https://github.com/jan-molak/authenticator-browser-extension/tree/master/e2e/webdriverio)) 21 | - [Protractor](https://www.protractortest.org/#/) ([example](https://github.com/jan-molak/authenticator-browser-extension/tree/master/e2e/protractor)) 22 | - [Puppeteer](https://github.com/puppeteer/puppeteer) ([example](https://github.com/jan-molak/authenticator-browser-extension/tree/master/e2e/puppeteer)) 23 | 24 | and following browsers: 25 | - [Google Chrome](https://www.google.co.uk/chrome/) ([example 1](https://github.com/jan-molak/authenticator-browser-extension/tree/master/e2e/protractor), [example 2](https://github.com/jan-molak/authenticator-browser-extension/tree/master/e2e/webdriverio)) 26 | - [Firefox Developer Edition](https://www.mozilla.org/en-GB/firefox/developer/) ([example](https://github.com/jan-molak/authenticator-browser-extension/tree/master/e2e/webdriverio)) 27 | 28 | It's possible that Authenticator will work with other browsers supporting [Web Extensions](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions) and [`webRequest.onAuthRequired`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onAuthRequired) API. However, I didn't have a chance to verify it yet. 29 | 30 | ## For Enterprise 31 | 32 | 33 | 34 | Authenticator is available as part of the [Tidelift Subscription](https://tidelift.com/subscription/pkg/npm-authenticator-browser-extension?utm_source=npm-authenticator-browser-extension&utm_medium=referral&utm_campaign=enterprise&utm_term=repo). The maintainers of Authenticator and thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. If you want the flexibility of open source and the confidence of commercial-grade software, this is for you. [Learn more.](https://tidelift.com/subscription/pkg/npm-authenticator-browser-extension?utm_source=npm-authenticator-browser-extension&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) 35 | 36 | ## Usage 37 | 38 | Install the module from npm: 39 | 40 | ``` 41 | npm install --save-dev authenticator-browser-extension 42 | ``` 43 | 44 | The best place to look for usage examples is the [e2e test suite](https://github.com/jan-molak/authenticator-browser-extension/tree/master/e2e). 45 | 46 | ### WebdriverIO 47 | 48 | Import the `authenticator-browser-extension` in the [`wdio.conf.js`](https://webdriver.io/docs/options.html) file and add `Authenticator` to the list of Chrome extensions: 49 | 50 | ```javascript 51 | // wdio.conf.js 52 | 53 | const { Authenticator } = require('authenticator-browser-extension'); 54 | 55 | exports.config = { 56 | 57 | capabilities: [{ 58 | browserName: 'chrome', 59 | 60 | 'goog:chromeOptions': { 61 | extensions: [ 62 | Authenticator.for('username', 'password').asBase64() 63 | ] 64 | } 65 | }], 66 | 67 | // other WebdriverIO config 68 | } 69 | ``` 70 | 71 | ### Protractor 72 | 73 | Import the `authenticator-browser-extension` in the [`protractor.conf.js`](https://www.protractortest.org/#/api-overview#example-config-file) file and add `Authenticator` to the list of Chrome extensions: 74 | 75 | ```javascript 76 | // protractor.conf.js 77 | 78 | const { Authenticator } = require('authenticator-browser-extension'); 79 | 80 | exports.config = { 81 | 82 | capabilities: { 83 | browserName: 'chrome', 84 | 85 | chromeOptions: { 86 | extensions: [ 87 | Authenticator.for('username', 'password').asBase64() 88 | ] 89 | } 90 | }, 91 | 92 | // other Protractor config 93 | } 94 | ``` 95 | 96 | ### Puppeteer 97 | 98 | Import the `authenticator-browser-extension` and generate an expanded `Authenticator` web extension directory before launching a Puppeteer browser: 99 | 100 | ```typescript 101 | // puppeteer/chrome-authenticator-extension.spec.ts 102 | const { Authenticator } = require('authenticator-browser-extension'); 103 | 104 | const authenticator = Authenticator.for('admin', 'Password123') 105 | .asDirectoryAt(`${process.cwd()}/build/puppeteer/authenticator`); 106 | 107 | browser = await puppeteer.launch({ 108 | headless: false, 109 | 110 | args: [ 111 | `--disable-extensions-except=${authenticator}`, 112 | `--load-extension=${authenticator}`, 113 | `--no-sandbox`, 114 | ], 115 | }); 116 | ``` 117 | 118 | ### Playwright 119 | 120 | Requires launching a [persistent browser context instance](https://playwright.dev/docs/api/class-browsertype?_highlight=persistent#browsertypelaunchpersistentcontextuserdatadir-options) containing the `Authenticator` extension. In every other way a carbon copy of the Puppeteer prototype. 121 | 122 | ```typescript 123 | // playwright/chrome-authenticator-extension.spec.ts 124 | const extensionDirectory = `${process.cwd()}/build/playwright/authenticator`; 125 | 126 | const authenticator = Authenticator.for( 127 | 'admin', 128 | 'Password123' 129 | ).asDirectoryAt(extensionDirectory); 130 | 131 | browser = await playwright['chromium'].launchPersistentContext( 132 | extensionDirectory, 133 | { 134 | args: [ 135 | `--disable-extensions-except=${authenticator}`, 136 | `--load-extension=${authenticator}`, 137 | `--no-sandbox`, 138 | ], 139 | headless: false, 140 | } 141 | ); 142 | ``` 143 | 144 | ## Known limitations 145 | 146 | ### Chrome headless 147 | 148 | Chrome doesn't support browser extensions when running in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome) and Chrome developers have [decided against](https://bugs.chromium.org/p/chromium/issues/detail?id=706008#c5) implementing this feature in any near future due to complexity of the task. 149 | 150 | The best way to get around this limitation is to use Chrome together with 151 | the [X Virtual Framebuffer (XVFB)](https://en.wikipedia.org/wiki/Xvfb). 152 | 153 | ### Firefox 154 | 155 | Authenticator generates the web extension dynamically on your machine, which means that the extension is not signed by Mozilla. For this reason, in order to use Authenticator, you need to configure Firefox with a `xpinstall.signatures.required` flag set to `false` (see [example](https://github.com/jan-molak/authenticator-browser-extension/tree/master/e2e/webdriverio)). 156 | 157 | **NOTE**: Firefox 48 (Pushed from Firefox 46) and newer [do not allow for unsigned extensions to be installed](https://wiki.mozilla.org/Add-ons/Extension_Signing#Timeline), so you need to use [Firefox Developer Edition](https://www.mozilla.org/en-GB/firefox/developer/) instead. 158 | 159 | ## Your feedback matters! 160 | 161 | Do you find Authenticator useful? Give it a star! ★ 162 | 163 | Found a bug? Need a feature? Raise [an issue](https://github.com/jan-molak/authenticator-browser-extension/issues?q=is%3Aopen) or submit a pull request. 164 | 165 | Have feedback? Let me know on twitter: [@JanMolak](https://twitter.com/JanMolak) 166 | 167 | ## Before you go 168 | 169 | ☕ If Authenticator has made your life a little bit easier and saved at least $5 worth of your time, 170 | please consider repaying the favour and [buying me a coffee](https://github.com/sponsors/jan-molak) via [Github Sponsors](https://github.com/sponsors/jan-molak). Thanks! 🙏 171 | 172 | ## License 173 | Authenticator library is licensed under the Apache-2.0 license. 174 | 175 | _- Copyright © 2019- [Jan Molak](https://janmolak.com)_ 176 | 177 | 178 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjan-molak%2Fauthenticator-browser-extension.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjan-molak%2Fauthenticator-browser-extension?ref=badge_large) 179 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /e2e/TestApp.ts: -------------------------------------------------------------------------------- 1 | import express = require('express'); 2 | import basicAuth = require('express-basic-auth'); 3 | 4 | export class TestApp { 5 | static allowingUsersAuthenticatedWith(credentials: { username: string; password: string }): express.Express { 6 | const app = express(); 7 | 8 | app.use(basicAuth({ 9 | users: { [credentials.username]: credentials.password }, 10 | challenge: true, // <--- needed to actually show the dialog box 11 | })); 12 | 13 | app.get('/', (request: express.Request, response: express.Response) => { 14 | response.send(` 15 | 16 | 17 | 18 |

Authenticated!

19 | 20 | 21 | `); 22 | }); 23 | 24 | return app; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /e2e/playwright/chrome-authenticator-extension.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-use-before-define,@typescript-eslint/no-explicit-any */ 2 | 3 | import { Ensure, equals } from '@serenity-js/assertions'; 4 | import { Ability, Actor, actorCalled, Cast, engage, Interaction, Question, UsesAbilities } from '@serenity-js/core'; 5 | import { LocalServer, ManageALocalServer, StartLocalServer, StopLocalServer } from '@serenity-js/local-server'; 6 | import { Browser, ElementHandle, Page, Response } from 'playwright'; 7 | 8 | import { Authenticator } from '../../src'; 9 | import { TestApp } from '../TestApp'; 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-var-requires 12 | const playwright = require('playwright'); 13 | 14 | let page: Page; 15 | let browser: Browser; 16 | 17 | describe('Chrome Authenticator Extension, when used with Playwright', function () { 18 | this.timeout(15000); 19 | 20 | before(async () => { 21 | const extensionDirectory = `${process.cwd()}/build/playwright/authenticator`; 22 | 23 | const authenticator = Authenticator.for( 24 | 'admin', 25 | 'Password123' 26 | ).asDirectoryAt(extensionDirectory); 27 | 28 | browser = await playwright['chromium'].launchPersistentContext( 29 | extensionDirectory, 30 | { 31 | args: [ 32 | `--disable-extensions-except=${authenticator}`, 33 | `--load-extension=${authenticator}`, 34 | `--no-sandbox`, 35 | ], 36 | headless: false, 37 | } 38 | ); 39 | page = await browser.newPage(); 40 | }); 41 | 42 | class Actors implements Cast { 43 | prepare(actor: Actor): Actor { 44 | return actor.whoCan( 45 | BrowseTheWeb.using(page), 46 | ManageALocalServer.runningAHttpListener( 47 | TestApp.allowingUsersAuthenticatedWith({ 48 | username: 'admin', 49 | password: 'Password123', 50 | }) 51 | ) 52 | ); 53 | } 54 | } 55 | 56 | beforeEach(() => engage(new Actors())); 57 | beforeEach(() => 58 | actorCalled('Dave').attemptsTo(StartLocalServer.onRandomPort()) 59 | ); 60 | 61 | it(`enables a Chrome web browser-based test to authenticate with a web app`, () => 62 | actorCalled('Dave').attemptsTo( 63 | Navigate.to(LocalServer.url()), 64 | Ensure.that(Text.of(TestPage.Title), equals('Authenticated!')) 65 | )); 66 | 67 | after(async () => await browser.close()); 68 | after(() => actorCalled('Dave').attemptsTo(StopLocalServer.ifRunning())); 69 | }); 70 | 71 | // Serenity/JS doesn't support Playwright natively yet. 72 | // However, below is a minimalists proof-of-concept Screenplay Pattern-style integration code 73 | // that brings the two frameworks together. 74 | // 75 | // If you'd like Serenity/JS to support Playwright out of the box, please: 76 | // - vote on https://github.com/serenity-js/serenity-js/issues/493 77 | // - ask your boss to sponsor this feature - https://github.com/sponsors/serenity-js 78 | 79 | const Navigate = { 80 | to: (url: Question) => 81 | Interaction.where( 82 | `#actor navigates to ${url}`, 83 | (actor) => 84 | actor 85 | .answer(url) 86 | .then((actualUrl) => BrowseTheWeb.as(actor).get(actualUrl)) 87 | .then((_: Response | null) => void 0) // eslint-disable-line @typescript-eslint/no-unused-vars 88 | ), 89 | }; 90 | 91 | const Target = { 92 | the: (name: string) => ({ 93 | locatedBy: (selector: string) => 94 | Question.about>(`the ${name}`, (actor) => 95 | BrowseTheWeb.as(actor).locate(selector) 96 | ), 97 | }), 98 | }; 99 | 100 | const Text = { 101 | of: (target: Question>) => 102 | Question.about>(`text of ${target}`, (actor) => 103 | actor.answer(target).then((element) => { 104 | return page.evaluate( 105 | (actualElement) => actualElement.textContent, 106 | element 107 | ); 108 | }) 109 | ), 110 | }; 111 | 112 | const TestPage = { 113 | Title: Target.the('header').locatedBy('h1'), 114 | }; 115 | 116 | class BrowseTheWeb implements Ability { 117 | static using(browserInstance: Page) { 118 | return new BrowseTheWeb(browserInstance); 119 | } 120 | 121 | static as(actor: UsesAbilities): BrowseTheWeb { 122 | return actor.abilityTo(BrowseTheWeb); 123 | } 124 | 125 | constructor(private readonly page: Page) { 126 | } 127 | 128 | get(destination: string): Promise { 129 | return this.page.goto(destination); 130 | } 131 | 132 | locate(selector: string): Promise { 133 | return this.page.$(selector); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /e2e/protractor/chrome-authenticator-extension.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | 3 | import { Ensure, equals } from '@serenity-js/assertions'; 4 | import { Actor, actorCalled, Cast, engage } from '@serenity-js/core'; 5 | import { LocalServer, ManageALocalServer, StartLocalServer } from '@serenity-js/local-server'; 6 | import { BrowseTheWeb, Navigate, Target, Text } from '@serenity-js/protractor'; 7 | import { by, protractor } from 'protractor'; 8 | 9 | import { TestApp } from '../TestApp'; 10 | 11 | describe('Chrome Authenticator Extension, when used with Protractor,', function () { 12 | 13 | this.timeout(5000); 14 | 15 | class Actors implements Cast { 16 | prepare(actor: Actor): Actor { 17 | return actor.whoCan( 18 | BrowseTheWeb.using(protractor.browser), 19 | ManageALocalServer.runningAHttpListener(TestApp.allowingUsersAuthenticatedWith({ 20 | username: 'admin', 21 | password: 'Password123', 22 | })), 23 | ); 24 | } 25 | } 26 | 27 | const TestPage = { 28 | Title: Target.the('header').located(by.css('h1')), 29 | }; 30 | 31 | beforeEach(() => engage(new Actors())); 32 | beforeEach(() => actorCalled('Dave').attemptsTo(StartLocalServer.onRandomPort())); 33 | 34 | it(`enables a Chrome web browser-based test to authenticate with a web app`, () => 35 | actorCalled('Dave').attemptsTo( 36 | Navigate.to(LocalServer.url()), 37 | Ensure.that(Text.of(TestPage.Title), equals('Authenticated!')), 38 | )); 39 | }); 40 | -------------------------------------------------------------------------------- /e2e/protractor/protractor.conf.js: -------------------------------------------------------------------------------- 1 | require('ts-node/register'); 2 | 3 | const { Authenticator } = require('../../lib'); 4 | 5 | exports.config = { 6 | chromeDriver: require('chromedriver/lib/chromedriver').path, 7 | SELENIUM_PROMISE_MANAGER: false, 8 | directConnect: true, 9 | 10 | allScriptsTimeout: 110000, 11 | framework: 'mocha', 12 | mochaOpts: { 13 | ui: 'bdd', 14 | reporter: 'spec', 15 | // require: 'ts-node/register' // this doesn't work due to breaking changes introduced in Mocha 6.0 16 | }, 17 | 18 | specs: [ '**/*.spec.ts' ], 19 | 20 | capabilities: { 21 | browserName: 'chrome', 22 | 23 | chromeOptions: { 24 | args: [ 25 | '--disable-infobars', 26 | '--no-sandbox', 27 | '--disable-gpu', 28 | '--window-size=1024x768', 29 | ], 30 | extensions: [ 31 | Authenticator.for('admin', 'Password123').asBase64() 32 | ] 33 | } 34 | }, 35 | 36 | onPrepare: () => { 37 | browser.waitForAngularEnabled(false); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /e2e/puppeteer/chrome-authenticator-extension.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-use-before-define,@typescript-eslint/no-explicit-any */ 2 | 3 | import { Ensure, equals } from '@serenity-js/assertions'; 4 | import { Ability, Actor, actorCalled, Cast, engage, Interaction, Question, UsesAbilities } from '@serenity-js/core'; 5 | import { LocalServer, ManageALocalServer, StartLocalServer, StopLocalServer } from '@serenity-js/local-server'; 6 | import { Browser, ElementHandle, HTTPResponse, Page } from 'puppeteer/lib/cjs/puppeteer/api-docs-entry'; 7 | 8 | /* eslint-disable-next-line @typescript-eslint/no-var-requires */ 9 | const puppeteer = require('puppeteer'); 10 | 11 | import { Authenticator } from '../../src'; 12 | import { TestApp } from '../TestApp'; 13 | 14 | let page: Page; 15 | let browser: Browser; 16 | 17 | describe('Chrome Authenticator Extension, when used with Puppeteer', function () { 18 | this.timeout(15000); 19 | 20 | before(async () => { 21 | const authenticator = Authenticator.for('admin', 'Password123') 22 | .asDirectoryAt(`${ process.cwd() }/build/puppeteer/authenticator`); 23 | 24 | browser = await puppeteer.launch({ 25 | headless: false, 26 | args: [ 27 | `--disable-extensions-except=${ authenticator }`, 28 | `--load-extension=${ authenticator }`, 29 | `--no-sandbox`, 30 | ], 31 | }); 32 | page = await browser.newPage(); 33 | }); 34 | 35 | class Actors implements Cast { 36 | prepare(actor: Actor): Actor { 37 | return actor.whoCan( 38 | BrowseTheWeb.using(page), 39 | ManageALocalServer.runningAHttpListener( 40 | TestApp.allowingUsersAuthenticatedWith({ 41 | username: 'admin', 42 | password: 'Password123', 43 | }), 44 | ), 45 | ); 46 | } 47 | } 48 | 49 | beforeEach(() => engage(new Actors())); 50 | beforeEach(() => 51 | actorCalled('Dave').attemptsTo(StartLocalServer.onRandomPort()), 52 | ); 53 | 54 | it(`enables a Chrome web browser-based test to authenticate with a web app`, () => 55 | actorCalled('Dave').attemptsTo( 56 | Navigate.to(LocalServer.url()), 57 | Ensure.that(Text.of(TestPage.Title), equals('Authenticated!')), 58 | )); 59 | 60 | after(async () => await browser.close()); 61 | after(() => actorCalled('Dave').attemptsTo(StopLocalServer.ifRunning())); 62 | }); 63 | 64 | // Serenity/JS doesn't support Puppeteer natively yet. 65 | // However, below is a minimalists proof-of-concept Screenplay Pattern-style integration code 66 | // that brings the two frameworks together. 67 | // 68 | // If you'd like Serenity/JS to support Puppeteer out of the box, please: 69 | // - vote on https://github.com/serenity-js/serenity-js/issues/493 70 | // - ask your boss to sponsor this feature - https://github.com/sponsors/serenity-js 71 | 72 | const Navigate = { 73 | to: (url: Question) => 74 | Interaction.where(`#actor navigates to ${ url }`, actor => 75 | actor 76 | .answer(url) 77 | .then(actualUrl => BrowseTheWeb.as(actor).get(actualUrl)) 78 | .then((_: HTTPResponse | null) => void 0), // eslint-disable-line @typescript-eslint/no-unused-vars 79 | ), 80 | }; 81 | 82 | const Target = { 83 | the: (name: string) => ({ 84 | locatedBy: (selector: string) => 85 | Question.about>(`the ${ name }`, actor => 86 | BrowseTheWeb.as(actor).locate(selector), 87 | ), 88 | }), 89 | }; 90 | 91 | const Text = { 92 | of: (target: Question>) => 93 | Question.about>(`text of ${ target }`, actor => 94 | actor.answer(target).then(element => { 95 | return page.evaluate((actualElement: any) => actualElement.textContent, element); 96 | }), 97 | ), 98 | }; 99 | 100 | const TestPage = { 101 | Title: Target.the('header').locatedBy('h1'), 102 | }; 103 | 104 | class BrowseTheWeb implements Ability { 105 | static using(browserInstance: Page) { 106 | return new BrowseTheWeb(browserInstance); 107 | } 108 | 109 | static as(actor: UsesAbilities): BrowseTheWeb { 110 | return actor.abilityTo(BrowseTheWeb); 111 | } 112 | 113 | constructor(private readonly page: Page) { 114 | } 115 | 116 | get(destination: string): Promise { 117 | return this.page.goto(destination); 118 | } 119 | 120 | locate(selector: string): Promise { 121 | return this.page.$(selector); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /e2e/webdriverio/chrome-authenticator-extension.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-use-before-define, @typescript-eslint/ban-types */ 2 | 3 | import 'mocha'; 4 | import 'webdriverio'; 5 | 6 | import { Ensure, equals } from '@serenity-js/assertions'; 7 | import { 8 | Ability, 9 | Actor, 10 | actorCalled, 11 | Cast, 12 | Duration, 13 | engage, 14 | Interaction, 15 | Question, 16 | UsesAbilities, 17 | } from '@serenity-js/core'; 18 | import { LocalServer, ManageALocalServer, StartLocalServer } from '@serenity-js/local-server'; 19 | import { BrowserObject, Element, MultiRemoteBrowser } from 'webdriverio'; 20 | 21 | import { TestApp } from '../TestApp'; 22 | 23 | describe('Authenticator Browser Extension, when used with WebDriver.io,', function () { 24 | 25 | this.timeout(30_000); 26 | 27 | class Actors implements Cast { 28 | prepare(actor: Actor): Actor { 29 | return actor.whoCan( 30 | BrowseTheWeb.using(browser), 31 | ManageALocalServer.runningAHttpListener(TestApp.allowingUsersAuthenticatedWith({ 32 | username: 'admin', 33 | password: 'Password123', 34 | })), 35 | ); 36 | } 37 | } 38 | 39 | beforeEach(() => engage(new Actors())); 40 | beforeEach(() => actorCalled('Dave').attemptsTo(StartLocalServer.onRandomPort())); 41 | 42 | it(`enables a web browser-based test to authenticate with a web app`, () => 43 | actorCalled('Dave').attemptsTo( 44 | Navigate.to(LocalServer.url()), 45 | Ensure.that(Text.of(TestPage.Title), equals('Authenticated!')), 46 | )); 47 | }); 48 | 49 | // Serenity/JS doesn't support WebdriverIO natively yet. 50 | // However, below is a minimalistic proof-of-concept Screenplay Pattern-style integration code 51 | // that brings the two frameworks together. 52 | // 53 | // If you'd like Serenity/JS to support WebdriverIO out of the box, please: 54 | // - vote on https://github.com/serenity-js/serenity-js/issues/493 55 | // - ask your boss to sponsor this feature - https://github.com/sponsors/serenity-js 56 | 57 | const Navigate = { 58 | to: (url: Question) => 59 | Interaction.where(`#actor navigates to ${ url }`, actor => 60 | actor.answer(url).then(actualUrl => BrowseTheWeb.as(actor).get(actualUrl)) 61 | ), 62 | }; 63 | 64 | const Target = { 65 | the: (name: string) => ({ 66 | locatedBy: (selector: string | Function | object) => 67 | Question.about>(`the ${ name }`, actor => 68 | BrowseTheWeb.as(actor).locate(selector), 69 | ), 70 | }), 71 | }; 72 | 73 | const Text = { 74 | of: (target: Question>) => 75 | Question.about>(`text of ${ target }`, actor => 76 | actor.answer(target).then(element => element.getText()), 77 | ), 78 | }; 79 | 80 | const TestPage = { 81 | Title: Target.the('header').locatedBy('h1'), 82 | }; 83 | 84 | class BrowseTheWeb implements Ability { 85 | static using(browserInstance: BrowserObject | MultiRemoteBrowser) { 86 | return new BrowseTheWeb(browserInstance); 87 | } 88 | 89 | static as(actor: UsesAbilities): BrowseTheWeb { 90 | return actor.abilityTo(BrowseTheWeb); 91 | } 92 | 93 | constructor(private readonly browserInstance: BrowserObject | MultiRemoteBrowser) { 94 | } 95 | 96 | get(destination: string): Promise { 97 | return this.browserInstance.url(destination); 98 | } 99 | 100 | locate(selector: string | Function | object): Promise { 101 | return this.browserInstance.$(selector); 102 | } 103 | 104 | sleep(durationInMillis: number) { 105 | return this.browserInstance.pause(durationInMillis); 106 | } 107 | } 108 | 109 | const Wait = { // eslint-disable-line @typescript-eslint/no-unused-vars 110 | for: (duration: Duration) => 111 | Interaction.where(`#actor waits for ${ duration }`, actor => 112 | BrowseTheWeb.as(actor).sleep(duration.inMilliseconds()), 113 | ), 114 | }; 115 | -------------------------------------------------------------------------------- /e2e/webdriverio/wdio.conf.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prevent-abbreviations */ 2 | import { Authenticator } from '../../lib'; 3 | 4 | exports.config = { 5 | runner: 'local', 6 | specs: [ 7 | 'e2e/webdriverio/**/*.spec.ts' 8 | ], 9 | 10 | maxInstances: 1, 11 | 12 | capabilities: [{ 13 | 14 | browserName: 'chrome', 15 | 'goog:chromeOptions': { 16 | args: [ 17 | '--disable-infobars', 18 | '--no-sandbox', 19 | '--disable-gpu', 20 | '--window-size=1024x768', 21 | ], 22 | extensions: [ 23 | Authenticator.for('admin', 'Password123').asBase64() 24 | ] 25 | } 26 | }, { 27 | browserName: 'firefox', 28 | }], 29 | 30 | logLevel: 'debug', 31 | 32 | waitforTimeout: 10000, 33 | 34 | connectionRetryTimeout: 90000, 35 | 36 | connectionRetryCount: 3, 37 | 38 | // Geckodriver config 39 | path: '/', 40 | 41 | // NOTE: Make sure to use Firefox Developer Edition - https://www.mozilla.org/en-GB/firefox/developer/ 42 | // and either add it to the PATH env variable, or specify below 43 | // Only Firefox Developer Edition supports custom, unsigned extensions. 44 | // geckoDriverArgs: ['--binary=/path/to/developer/edition/of/firefox'], 45 | 46 | services: [ 47 | 'chromedriver', 48 | 'geckodriver', 49 | 50 | ['firefox-profile', { 51 | extensions: [ 52 | Authenticator.for('admin', 'Password123') 53 | .asFileAt('build/wdio/authenticator.xpi') 54 | ], 55 | // NOTE: this option is required to load an unsigned extension 56 | 'xpinstall.signatures.required': false 57 | }], 58 | ], 59 | 60 | framework: 'mocha', 61 | reporters: ['spec'], 62 | 63 | mochaOpts: { 64 | ui: 'bdd', 65 | timeout: 60000, 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /extension/authenticator.mustache.js: -------------------------------------------------------------------------------- 1 | // todo: "pending" https://github.com/mdn/webextensions-examples/blob/master/stored-credentials/auth.js 2 | 3 | let maxRetries = 3; // todo: configurable? 4 | 5 | (chrome || browser).webRequest.onAuthRequired.addListener( 6 | /** 7 | * @param details - see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onAuthRequired#details 8 | */ 9 | 10 | // don't escape password chars when rendering (since symbols are expected and must be preserved) 11 | function authenticator(details) { 12 | return (--maxRetries < 0) 13 | ? { cancel: true } 14 | : { authCredentials: { username: `{{ username }}`, password: `{{{ password }}}` }}; 15 | }, 16 | { urls: [ '' ]}, 17 | [ 'blocking' ], 18 | ); 19 | -------------------------------------------------------------------------------- /extension/manifest.mustache.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ name }}", 3 | "description": "{{ description }}", 4 | "version": "{{ version }}", 5 | "manifest_version": 2, 6 | "permissions": [ {{{ permissions }}}, "webRequest", "webRequestBlocking", "proxy" ], 7 | "background": { 8 | "scripts": [ "authenticator.js" ] 9 | }, 10 | "browser_specific_settings": { 11 | "gecko": { 12 | "id": "{{ name }}@smartcodeltd.co.uk" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authenticator-browser-extension", 3 | "version": "0.0.0-development", 4 | "description": "Enables your browser-based automated tests to authenticate with your web app.", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "funding": { 8 | "url": "https://github.com/sponsors/jan-molak" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf build reports", 12 | "lint": "eslint . --ext ts", 13 | "lint:fix": "eslint . --ext ts --fix", 14 | "compile": "tsc --project tsconfig.json", 15 | "test:spec": "nyc --report-dir ./reports/coverage mocha 'spec/**/*.spec.ts'", 16 | "test:e2e": "npm run test:e2e:protractor && npm run test:e2e:wdio && npm run test:e2e:puppeteer && npm run test:e2e:playwright", 17 | "test:e2e:protractor": "protractor e2e/protractor/protractor.conf.js", 18 | "test:e2e:wdio": "wdio e2e/webdriverio/wdio.conf.ts", 19 | "test:e2e:playwright": "mocha e2e/playwright/chrome-authenticator-extension.spec.ts", 20 | "test:e2e:puppeteer": "mocha e2e/puppeteer/chrome-authenticator-extension.spec.ts", 21 | "test": "npm run test:spec", 22 | "verify": "npm run clean && npm run lint && npm run test:spec && npm run compile && npm run test:e2e", 23 | "publish:reports": "nyc report --reporter=text-lcov | coveralls", 24 | "commit": "git-cz", 25 | "semantic-release": "semantic-release" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/jan-molak/authenticator-browser-extension.git" 30 | }, 31 | "keywords": [ 32 | "chrome", 33 | "protractor", 34 | "webdriver", 35 | "wdio", 36 | "plugin", 37 | "extension" 38 | ], 39 | "author": "Jan Molak ", 40 | "license": "Apache-2.0", 41 | "bugs": { 42 | "url": "https://github.com/jan-molak/authenticator-browser-extension/issues" 43 | }, 44 | "homepage": "https://github.com/jan-molak/authenticator-browser-extension#readme", 45 | "devDependencies": { 46 | "@serenity-js/assertions": "^2.31.1", 47 | "@serenity-js/core": "^2.31.1", 48 | "@serenity-js/local-server": "^2.31.1", 49 | "@serenity-js/protractor": "^2.31.1", 50 | "@types/chai": "^4.2.21", 51 | "@types/express": "^4.17.13", 52 | "@types/graceful-fs": "^4.1.5", 53 | "@types/mocha": "^9.0.0", 54 | "@types/mustache": "^4.1.2", 55 | "@types/node": "^14.17.11", 56 | "@types/semver": "^7.3.8", 57 | "@typescript-eslint/eslint-plugin": "^4.29.2", 58 | "@typescript-eslint/parser": "^4.29.2", 59 | "@wdio/cli": "^7.10.0", 60 | "@wdio/firefox-profile-service": "^7.8.0", 61 | "@wdio/local-runner": "^7.10.0", 62 | "@wdio/mocha-framework": "^7.10.0", 63 | "@wdio/spec-reporter": "^7.10.0", 64 | "chai": "^4.3.4", 65 | "chromedriver": "^92.0.1", 66 | "commitizen": "^4.2.4", 67 | "coveralls": "^3.1.1", 68 | "cz-conventional-changelog": "^3.3.0", 69 | "eslint": "^7.32.0", 70 | "eslint-plugin-simple-import-sort": "^7.0.0", 71 | "eslint-plugin-unicorn": "^28.0.2", 72 | "express": "^4.17.1", 73 | "express-basic-auth": "^1.2.0", 74 | "geckodriver": "^2.0.3", 75 | "memfs": "^3.2.2", 76 | "mocha": "^9.1.0", 77 | "mocha-testdata": "^1.2.0", 78 | "nyc": "^15.1.0", 79 | "playwright": "^1.14.0", 80 | "protractor": "^7.0.0", 81 | "puppeteer": "^10.2.0", 82 | "rimraf": "^3.0.2", 83 | "semantic-release": "^17.4.5", 84 | "semantic-release-cli": "^5.4.3", 85 | "ts-node": "^10.2.1", 86 | "typescript": "^4.3.5", 87 | "wdio-chromedriver-service": "^7.2.0", 88 | "wdio-geckodriver-service": "^2.0.3", 89 | "webdriverio": "^7.10.0" 90 | }, 91 | "dependencies": { 92 | "graceful-fs": "^4.2.8", 93 | "mustache": "^4.2.0", 94 | "node-zip": "^1.1.1", 95 | "read-pkg": "^5.2.0", 96 | "semver": "^7.3.5", 97 | "tiny-types": "^1.16.1", 98 | "upath": "^2.0.1" 99 | }, 100 | "engines": { 101 | "node": "^12 || ^14 || ^16" 102 | }, 103 | "config": { 104 | "commitizen": { 105 | "path": "cz-conventional-changelog" 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /spec/Authenticator.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define, unicorn/consistent-function-scoping */ 2 | 3 | import 'mocha'; 4 | 5 | import { expect } from 'chai'; 6 | import * as fs from 'fs'; 7 | import { createFsFromVolume, DirectoryJSON, Volume } from 'memfs'; 8 | import { given } from 'mocha-testdata'; 9 | import path = require('upath'); 10 | import readPkg = require('read-pkg'); // eslint-disable-line unicorn/prevent-abbreviations 11 | import { Authenticator } from '../src'; 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-var-requires -- no type definitions available 14 | const Zip = require('node-zip'); 15 | 16 | describe('Authenticator', () => { 17 | 18 | describe('#asBase64', () => { 19 | 20 | describe('dynamically generates a base64-encoded web extension file that', () => { 21 | 22 | it('contains a manifest file with the name, description and version of the project using the Authenticator', () => { 23 | const zip = asZip(Authenticator.for('user', 'pass').asBase64()); 24 | const pkg = readPkg.sync({ cwd: path.resolve(__dirname, '..') }); // eslint-disable-line unicorn/prevent-abbreviations 25 | 26 | const manifest = JSON.parse(zip.files['manifest.json']._data); 27 | 28 | expect(manifest.description).to.deep.equal(pkg.description); 29 | expect(manifest.name).to.deep.equal(pkg.name); 30 | expect(manifest.version).to.match(/(\d\.?){3}/); 31 | }); 32 | 33 | it('contains an authenticator script that includes the desired credentials', () => { 34 | const zip = asZip(Authenticator.for('user', 'pass').asBase64()); 35 | 36 | // authenticator script 37 | const authenticator = zip.files['authenticator.js']._data; 38 | 39 | expect(authenticator).to.contain( 40 | '{ authCredentials: { username: `user`, password: `pass` }}' 41 | ); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('#asFileAt', () => { 47 | const 48 | cwd = process.cwd(), 49 | relativePathToExtensionFile = `./build/extensions/authenticator.xpi`, 50 | absolutePathToExtensionFile = path.join(process.cwd(), relativePathToExtensionFile); 51 | 52 | let fakeFS: typeof fs, 53 | authenticator: Authenticator; 54 | 55 | beforeEach(() => { 56 | fakeFS = fakeFSWith({ 57 | 'extension/authenticator.mustache.js': contentsOf('extension/authenticator.mustache.js'), 58 | 'extension/manifest.mustache.json': contentsOf('extension/manifest.mustache.json'), 59 | }, cwd); 60 | 61 | authenticator = new Authenticator( 62 | 'user', 63 | 'pass', 64 | [''], 65 | cwd, 66 | fakeFS, 67 | ); 68 | }); 69 | 70 | it('generates an .xpi file at a specified location', () => { 71 | const result = authenticator.asFileAt(relativePathToExtensionFile); 72 | 73 | expect(result).to.equal(absolutePathToExtensionFile); 74 | expect(fakeFS.existsSync(result)).equals(true); 75 | }); 76 | 77 | it('allows for the file mode to be configured', () => { 78 | const mode644 = 0o100644; 79 | 80 | const result = authenticator.asFileAt(relativePathToExtensionFile, mode644); 81 | 82 | const stat = fakeFS.statSync(result); 83 | 84 | expect(stat.mode).equals(mode644); 85 | }); 86 | 87 | it('complains if the relative path is empty', () => { 88 | expect(() => authenticator.asFileAt('')) 89 | .to.throw('path to extension file should have a property "length" that is greater than 0'); 90 | }); 91 | }); 92 | 93 | describe('#asDirectoryAt', () => { 94 | const 95 | cwd = process.cwd(), 96 | relativePathToExtensionDirectory = `./build/extensions/authenticator`, 97 | absolutePathToExtensionDirectory = path.join(process.cwd(), relativePathToExtensionDirectory); 98 | 99 | let fakeFS: typeof fs, 100 | authenticator: Authenticator; 101 | 102 | beforeEach(() => { 103 | // copy the template files to fakeFS so that Authenticator can load them 104 | fakeFS = fakeFSWith({ 105 | 'extension/authenticator.mustache.js': contentsOf('extension/authenticator.mustache.js'), 106 | 'extension/manifest.mustache.json': contentsOf('extension/manifest.mustache.json'), 107 | }, cwd); 108 | 109 | authenticator = new Authenticator( 110 | 'user', 111 | 'pass', 112 | [''], 113 | cwd, 114 | fakeFS, 115 | ); 116 | }); 117 | 118 | it('allows for an extension directory to be generated in a directory at a specified location', () => { 119 | const result = authenticator.asDirectoryAt(relativePathToExtensionDirectory); 120 | 121 | expect(result).to.equal(absolutePathToExtensionDirectory); 122 | 123 | expect(fakeFS.existsSync(path.resolve(absolutePathToExtensionDirectory, 'manifest.json'))).equals(true); 124 | expect(fakeFS.existsSync(path.resolve(absolutePathToExtensionDirectory, 'authenticator.js'))).equals(true); 125 | }); 126 | 127 | it('allows for the file mode to be configured', () => { 128 | const mode644 = 0o440644; 129 | 130 | const result = authenticator.asDirectoryAt(relativePathToExtensionDirectory, mode644); 131 | 132 | const stat = fakeFS.statSync(result); 133 | 134 | expect(stat.mode).equals(mode644); 135 | }); 136 | 137 | it('complains if the relative path is empty', () => { 138 | expect(() => authenticator.asDirectoryAt('')) 139 | .to.throw('path to destination directory should have a property "length" that is greater than 0'); 140 | }); 141 | }); 142 | 143 | describe('permissions', () => { 144 | 145 | it('applies to all URLs by default', () => { 146 | const zip = asZip(Authenticator.for('user', 'pass').asBase64()); 147 | 148 | const manifest = JSON.parse(zip.files['manifest.json']._data); 149 | 150 | expect(manifest.permissions).to.contain(''); 151 | }); 152 | 153 | it('allows the developer to restrict the extension to specific URLs', () => { 154 | const zip = asZip(Authenticator.for('user', 'pass', [ 'http://localhost/' ]).asBase64()); 155 | 156 | const manifest = JSON.parse(zip.files['manifest.json']._data); 157 | 158 | expect(manifest.permissions).to.contain('http://localhost/'); 159 | expect(manifest.permissions).to.not.contain(''); 160 | }); 161 | 162 | it('complains when given no permissions', () => { 163 | expect(() => Authenticator.for('user', 'pass', [ ])) 164 | .to.throw('permissions should have a property "length" that is greater than 0'); 165 | }); 166 | }); 167 | 168 | describe('when handling errors', () => { 169 | 170 | /* eslint-disable @typescript-eslint/indent, unicorn/no-null */ 171 | given([ 172 | { value: null, expected: 'username should be a string', }, 173 | { value: undefined, expected: 'username should be a string', }, 174 | { value: '', expected: 'username should have a property "length" that is greater than 0', }, 175 | { value: {}, expected: 'username should be a string', }, 176 | { value: [], expected: 'username should be a string', }, 177 | ]). 178 | it('complains if provided with an invalid username', ({ value, expected }: { value: unknown; expected: string }) => { 179 | expect(() => Authenticator.for(value as string, 'password')).to.throw(expected); 180 | }); 181 | 182 | given([ 183 | { value: null, expected: 'password should be a string', }, 184 | { value: undefined, expected: 'password should be a string', }, 185 | { value: '', expected: 'password should have a property "length" that is greater than 0' }, 186 | { value: {}, expected: 'password should be a string', }, 187 | { value: [], expected: 'password should be a string', }, 188 | ]). 189 | it('complains if provided with an invalid password', ({ value, expected }: { value: unknown; expected: string }) => { 190 | expect(() => Authenticator.for('username', value as string)).to.throw(expected) 191 | }); 192 | /* eslint-enable @typescript-eslint/indent */ 193 | }); 194 | 195 | function asZip(data: string): { files: {[filename: string]: { _data: string }}} { 196 | return new Zip(data, { base64: true, checkCRC32: true }); 197 | } 198 | 199 | function fakeFSWith(tree: DirectoryJSON, cwd: string): typeof fs { 200 | return createFsFromVolume(Volume.fromJSON(tree, cwd)) as unknown as typeof fs; 201 | } 202 | 203 | function contentsOf(pathToFile: string): string { 204 | return fs.readFileSync(path.resolve(process.cwd(), pathToFile)).toString('utf-8') 205 | } 206 | }); 207 | -------------------------------------------------------------------------------- /src/Authenticator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define, unicorn/prevent-abbreviations */ 2 | 3 | import * as nodeFS from 'fs'; 4 | import * as gracefulFS from 'graceful-fs'; 5 | import Mustache = require('mustache'); 6 | import readPkg = require('read-pkg'); 7 | import path = require('upath'); 8 | import { coerce, SemVer } from 'semver'; 9 | import { endsWith, ensure, isArray, isDefined, isGreaterThan, isNumber, isString, or, property } from 'tiny-types'; 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-var-requires -- no type definitions available 12 | const Zip = require('node-zip'); 13 | 14 | export class Authenticator { 15 | /** 16 | * @param {string} username 17 | * @param {string} password 18 | * @param {string[]} permissions 19 | * See https://developer.chrome.com/extensions/declare_permissions 20 | */ 21 | static for(username: string, password: string, permissions: string[] = ['']): Authenticator { 22 | return new Authenticator(username, password, permissions); 23 | } 24 | 25 | asBase64(): string { 26 | return this.extensionFile().generate({ base64: true, compression: 'DEFLATE' }); 27 | } 28 | 29 | asFileAt( 30 | relativePathToExtensionFile: string, 31 | mode: number = Number.parseInt('0777', 8) & (~process.umask()), 32 | ): string { 33 | 34 | ensure('path to extension file', relativePathToExtensionFile, 35 | isString(), 36 | property('length', isGreaterThan(0)), 37 | or(endsWith('.xpi'), endsWith('.crx')), 38 | ); 39 | 40 | ensure('mode', mode, isNumber()); 41 | 42 | const fullPath = path.resolve(this.cwd, relativePathToExtensionFile); 43 | 44 | this.fs.mkdirSync(path.dirname(fullPath), { recursive: true, mode }); 45 | 46 | const data = this.extensionFile().generate({ base64: false, compression: 'STORE' }); 47 | 48 | this.fs.writeFileSync(fullPath, data, { encoding: 'binary', mode }); 49 | 50 | return fullPath; 51 | } 52 | 53 | asDirectoryAt( 54 | relativePathToDestinationDirectory: string, 55 | mode: number = Number.parseInt('0777', 8) & (~process.umask()), 56 | ): string { 57 | 58 | ensure('path to destination directory', relativePathToDestinationDirectory, 59 | isString(), property('length', isGreaterThan(0)) 60 | ); 61 | 62 | ensure('mode', mode, isNumber()); 63 | 64 | const fullPath = path.resolve(this.cwd, relativePathToDestinationDirectory); 65 | 66 | this.fs.mkdirSync(fullPath, { recursive: true, mode }); 67 | 68 | this.fs.writeFileSync(path.resolve(fullPath, 'manifest.json'), this.authenticatorManifest()); 69 | this.fs.writeFileSync(path.resolve(fullPath, 'authenticator.js'), this.authenticatorScript()); 70 | 71 | return fullPath; 72 | } 73 | 74 | public constructor( 75 | private readonly username: string, 76 | private readonly password: string, 77 | private readonly permissions: string[], 78 | private readonly cwd: string = process.cwd(), 79 | private readonly fs: typeof nodeFS = gracefulFS, 80 | ) { 81 | ensure('username', username, isString(), property('length', isGreaterThan(0))); 82 | ensure('password', password, isString(), property('length', isGreaterThan(0))); 83 | ensure('permissions', permissions, isArray(), property('length', isGreaterThan(0))); 84 | ensure('cwd', cwd, isString(), property('length', isGreaterThan(0))); 85 | ensure('fs', cwd, isDefined()); 86 | } 87 | 88 | private extensionFile(): NodeZip { 89 | const zip: NodeZip = new Zip(); 90 | 91 | zip.file('manifest.json', this.authenticatorManifest()); 92 | zip.file('authenticator.js', this.authenticatorScript()); 93 | 94 | return zip; 95 | } 96 | 97 | private authenticatorManifest(): string { 98 | const { name, description, version } = readPkg.sync({ cwd: this.cwd }); 99 | 100 | return Mustache.render( 101 | this.contentsOf('../extension/manifest.mustache.json'), { 102 | name, 103 | description, 104 | permissions: this.permissions.map(permission => `"${ permission }"`).join(', '), 105 | version: (coerce(version as string) as SemVer).version, 106 | }, 107 | ) 108 | } 109 | 110 | private authenticatorScript(): string { 111 | return Mustache.render( 112 | this.contentsOf('../extension/authenticator.mustache.js'), 113 | { username: this.username, password: this.password }, 114 | ) 115 | } 116 | 117 | private contentsOf(fileName: string): string { 118 | return this.fs.readFileSync(path.join(__dirname, fileName)).toString('utf8'); 119 | } 120 | } 121 | 122 | interface NodeZip { 123 | file(name: string, contents: string | Buffer): void; 124 | 125 | /** 126 | * https://github.com/Stuk/jszip/blob/3109282aed65d902188086f2d37a009ce9eb268c/documentation/api_jszip/generate_async.md#compression-and-compressionoptions-options 127 | * @param options 128 | */ 129 | generate(options: { base64: boolean; compression: 'DEFLATE' | 'STORE' }): string; 130 | } 131 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Authenticator'; 2 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "spec/**/*.ts", 6 | "e2e/**/*.ts" 7 | ], 8 | 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ "es5", "es6" ], 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "outDir": "./lib", 11 | "skipLibCheck":true 12 | }, 13 | 14 | "include": [ 15 | "src/**/*.ts" 16 | ], 17 | 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rulesDirectory": "node_modules/tslint-microsoft-contrib", 4 | "rules": { 5 | "no-implicit-dependencies": [true, "dev"], 6 | "quotemark": [true, "single", "avoid-escape"], 7 | "one-variable-per-declaration": false, 8 | "member-access": false, 9 | "typedef-whitespace": 10 | [ 11 | true, 12 | { 13 | "call-signature": "nospace", 14 | "index-signature": "nospace", 15 | "parameter": "nospace", 16 | "property-declaration": "nospace", 17 | "variable-declaration": "nospace" 18 | }, 19 | { 20 | "call-signature": "onespace", 21 | "index-signature": "onespace", 22 | "parameter": "onespace", 23 | "property-declaration": "space", 24 | "variable-declaration": "space" 25 | } 26 | ], 27 | "object-literal-sort-keys": false, 28 | "no-bitwise": false, 29 | "one-line": false, 30 | "variable-name": [true, "ban-keywords"], 31 | "interface-name": [true, "never-prefix"], 32 | "max-line-length": [true, 180], 33 | "member-ordering": [true, { 34 | "order": [ 35 | "public-static-field", 36 | "public-instance-field", 37 | "public-constructor", 38 | "private-static-field", 39 | "private-instance-field", 40 | "public-instance-method", 41 | "private-constructor", 42 | "protected-instance-method", 43 | "private-instance-method" 44 | ], 45 | "alphabetize": false 46 | }], 47 | "only-arrow-functions": false, 48 | "arrow-parens": [true, "ban-single-arg-parens" ], 49 | "max-classes-per-file": false, 50 | "mocha-avoid-only": true 51 | } 52 | } 53 | --------------------------------------------------------------------------------