├── .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 | [](https://badge.fury.io/js/authenticator-browser-extension)
4 | [](https://github.com/jan-molak/authenticator-browser-extension/actions)
5 | [](https://coveralls.io/github/jan-molak/authenticator-browser-extension)
6 | [](https://npm-stat.com/charts.html?package=authenticator-browser-extension)
7 | [](https://snyk.io/test/github/jan-molak/authenticator-browser-extension)
8 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fjan-molak%2Fauthenticator-browser-extension?ref=badge_shield)
9 | [](https://github.com/semantic-release/semantic-release)
10 |
11 | [](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 | [](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 |
--------------------------------------------------------------------------------