-
Notifications
You must be signed in to change notification settings - Fork 0
feat(extension): add offline authenticator for chrome and firefox #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| name: Release Extension | ||
|
|
||
| on: | ||
| push: | ||
| branches: ["main"] | ||
| paths: | ||
| - "apps/extension/package.json" | ||
| - ".github/workflows/release-extension.yml" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| concurrency: | ||
| group: release-extension | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| prepare: | ||
| name: Prepare release | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| changed: ${{ steps.version.outputs.changed }} | ||
| tag: ${{ steps.version.outputs.tag }} | ||
| version: ${{ steps.version.outputs.version }} | ||
| steps: | ||
| - name: Check out code | ||
| uses: actions/checkout@v7 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Detect version change | ||
| id: version | ||
| shell: bash | ||
| run: | | ||
| current_version="$(jq -r '.version' apps/extension/package.json)" | ||
|
|
||
| if [[ "${{ github.event_name }}" != "push" ]]; then | ||
| echo "changed=true" >> "$GITHUB_OUTPUT" | ||
| echo "tag=ext-v$current_version" >> "$GITHUB_OUTPUT" | ||
| echo "version=$current_version" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| previous_version="$( | ||
| git show "${{ github.event.before }}:apps/extension/package.json" 2>/dev/null | | ||
| jq -r '.version' || | ||
| true | ||
| )" | ||
|
Comment on lines
+36
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Workflow around detected lines:\n'
sed -n '1,90p' .github/workflows/release-extension.yml
printf '\nSearch for release-extension reference and outputs:\n'
rg -n "release-extension|version|changed|tag=ext-v|previous_version|git show|github\.event\.before|github\.event_name" .github apps/extension -SRepository: denizlg24/denizlg24.com Length of output: 18017 🌐 Web query:
💡 Result: In GitHub Actions, the github.event.before property is set to an all-zero SHA (0000000000000000000000000000000000000000) when there is no previous commit reference for the event [1]. This most commonly occurs during the first push to a new branch, the creation of a new tag, or following a force push [2][3][1]. Because this all-zero string is not a valid git object, workflows that attempt to use github.event.before directly in commands like git fetch or git diff will encounter fatal errors [4][2][3]. To handle this, developers commonly implement conditional logic to detect the null SHA and provide a fallback [3][5][6]. Common strategies include: 1. Skipping the command: If the operation is intended to compare changes between commits, skip the operation entirely when the before SHA is null, as there is no prior state to compare against [4][7]. 2. Using an empty tree: For git diff commands, substituting the null SHA with the git empty-tree SHA (4b825dc642cb6eb9a060e54bf8d69288fbee4904) can allow the diff to treat all files in the initial commit as newly added [2]. 3. Falling back to an alternative base: Some workflows fall back to a default branch (e.g., origin/main), the parent of the first commit in the push, or a recent commit timestamp to establish a base [3][5][8]. Example of defensive logic in a workflow: if [[ "${{ github.event.before }}" == "0000000000000000000000000000000000000000" ]]; then echo "First push detected, skipping base comparison." else git diff "${{ github.event.before }}" "${{ github.sha }}" fi Citations:
🌐 Web query:
💡 Result: Using GitHub Actions context variables—such as github.event.before—directly within run steps creates a security risk known as script or shell injection [1][2][3]. This occurs because GitHub Actions evaluates the ${{ }} expression and substitutes its raw value into your shell script before the shell executes it [1][4][5]. If an attacker can influence that value to contain shell metacharacters (e.g., quotes, semicolons, backticks, or subshell syntax), they can break out of the intended command string and execute arbitrary code on the runner [1][2][3]. While github.event.before is a SHA reference and therefore less likely to be directly manipulated by an attacker compared to fields like issue titles or pull request bodies, it is still considered a best practice to avoid direct interpolation for any context variable [6][7][5]. Security tools and hardening guides flag this pattern because it violates defensive programming principles [6][8][9]. To prevent shell injection, you should always avoid using the ${{ }} syntax directly in run commands [10][5][3]. Instead, pass the context value into an environment variable using the env: block [2][4][3]. Because environment variables are handled as data rather than script code, the shell will not interpret metacharacters within the variable's value [11][5][3]. Recommended pattern: # ❌ VULNERABLE: Direct interpolation run: git diff --name-only ${{ github.event.before }} ${{ github.sha }} # ✅ SECURE: Using environment variables env: BEFORE_SHA: ${{ github.event.before }} run: git diff --name-only "$BEFORE_SHA" "${{ github.sha }}" By assigning the value to an environment variable, the shell treats the content as a safe string [11][3]. Additionally, quoting the variable (e.g., "$BEFORE_SHA") is a standard shell best practice to prevent word splitting [10][5]. Automated security scanners such as CodeQL and zizmor are designed to detect unsafe inline interpolation and should be used to audit your workflows [2][10][6][7]. Citations:
🌐 Web query:
💡 Result: In GitHub Actions, Citations:
🌐 Web query:
💡 Result: In a GitHub push event webhook payload, the fields appear in the JSON object at the top level [1][2]. While JSON objects are technically unordered collections of key-value pairs, the standard schema for a push event includes the following core fields [1][3]: ref: The full Git reference that was pushed (e.g., refs/heads/main or refs/tags/v3.14.1) [1][4]. before: The SHA of the most recent commit on the ref before the push [1][5]. after: The SHA of the most recent commit on the ref after the push [1][3]. These fields are essential for identifying the state of the branch or tag before and after the specific push action occurred [1][6]. When receiving the payload, you can use these SHAs to compare the changes included in the push [1][3]. [1][2] Citations:
Injection (CWE-94): Improper Control of Generation of Code ('Code Injection') Pass event data through
🧰 Tools🪛 zizmor (1.28.0)[warning] 38-38: code injection via template expansion (template-injection): may expand into attacker-controllable code (template-injection) [warning] 46-46: code injection via template expansion (template-injection): may expand into attacker-controllable code (template-injection) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| if [[ "$current_version" == "$previous_version" ]]; then | ||
| echo "changed=false" >> "$GITHUB_OUTPUT" | ||
| echo "The release workflow changed, but the extension version did not." | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "changed=true" >> "$GITHUB_OUTPUT" | ||
| echo "tag=ext-v$current_version" >> "$GITHUB_OUTPUT" | ||
| echo "version=$current_version" >> "$GITHUB_OUTPUT" | ||
|
|
||
| build: | ||
| name: Build and verify | ||
| needs: prepare | ||
| if: needs.prepare.outputs.changed == 'true' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Check out code | ||
| uses: actions/checkout@v7 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Install Bun | ||
| uses: oven-sh/setup-bun@v2 | ||
| with: | ||
| bun-version: 1.3.3 | ||
|
|
||
| - name: Install dependencies | ||
| run: bun install --frozen-lockfile --ignore-scripts | ||
|
|
||
| - name: Typecheck | ||
| run: bun run typecheck | ||
| working-directory: apps/extension | ||
|
|
||
| - name: Test | ||
| run: bun test | ||
| working-directory: apps/extension | ||
|
|
||
| - name: Package | ||
| run: bun run package | ||
| working-directory: apps/extension | ||
|
|
||
| - name: Check the manifests carry the released version | ||
| shell: bash | ||
| run: | | ||
| for target in chrome firefox; do | ||
| manifest_version="$(jq -r '.version' "apps/extension/dist/$target/manifest.json")" | ||
| if [[ "$manifest_version" != "${{ needs.prepare.outputs.version }}" ]]; then | ||
| echo "$target manifest is $manifest_version, expected ${{ needs.prepare.outputs.version }}" | ||
| exit 1 | ||
| fi | ||
| done | ||
|
|
||
| - name: Upload artifacts | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: extension-packages | ||
| path: apps/extension/release/*.zip | ||
| if-no-files-found: error | ||
|
|
||
| release: | ||
| name: Publish release | ||
| needs: [prepare, build] | ||
| if: needs.prepare.outputs.changed == 'true' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Check out code | ||
| uses: actions/checkout@v7 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Download artifacts | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: extension-packages | ||
| path: artifacts | ||
|
|
||
| - name: Build release notes | ||
| env: | ||
| TAG: ${{ needs.prepare.outputs.tag }} | ||
| VERSION: ${{ needs.prepare.outputs.version }} | ||
| shell: bash | ||
| run: | | ||
| previous_tag="$( | ||
| git tag --list 'ext-v*' --sort=-v:refname | | ||
| grep -Fvx "$TAG" | | ||
| head -n 1 || | ||
| true | ||
| )" | ||
|
|
||
| if [[ -n "$previous_tag" ]]; then | ||
| changes="$(git log --format='- %s (`%h`)' "$previous_tag..HEAD" -- apps/extension)" | ||
| else | ||
| changes="$(git log --format='- %s (`%h`)' HEAD -- apps/extension)" | ||
| fi | ||
|
|
||
| if [[ -z "$changes" ]]; then | ||
| changes="- Release metadata only." | ||
| fi | ||
|
|
||
| { | ||
| echo "## Authenticator extension v$VERSION" | ||
| echo | ||
| echo "$changes" | ||
| echo | ||
| echo "Upload \`*-chrome-*.zip\` to the Chrome Web Store and" | ||
| echo "\`*-firefox-*.zip\` to AMO, with \`*-source-*.zip\` as the" | ||
| echo "required source submission. Submission notes:" | ||
| echo "https://github.com/${{ github.repository }}/blob/$TAG/apps/extension/store/listing.md" | ||
| } > extension-release-notes.md | ||
|
|
||
| - name: Create GitHub release | ||
| uses: softprops/action-gh-release@v2 | ||
| with: | ||
| tag_name: ${{ needs.prepare.outputs.tag }} | ||
| target_commitish: ${{ github.sha }} | ||
| name: Authenticator extension v${{ needs.prepare.outputs.version }} | ||
| body_path: extension-release-notes.md | ||
| make_latest: false | ||
| files: artifacts/*.zip | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| dist/ | ||
| release/ | ||
| # Regenerated by scripts/generate-icons.ts on every build; SOURCE.md tells | ||
| # reviewers no binary assets are checked in, so keep it true. | ||
| public/icons/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Authenticator extension | ||
|
|
||
| Offline-first TOTP codes for Chrome and Firefox, synced with | ||
| `/api/admin/authenticator` on denizlg24.com. | ||
|
|
||
| ## How it works | ||
|
|
||
| Secrets are pulled once from `GET /api/admin/authenticator/export`, sealed with | ||
| AES-256-GCM under a key derived from a passphrase (PBKDF2-SHA256, 600k | ||
| iterations), and kept in `storage.local`. Codes are generated locally, so the | ||
| extension keeps working when the server does not — that is the point of holding | ||
| a copy rather than polling for codes. | ||
|
|
||
| The unlocked key lives in `storage.session` (memory only, extension contexts | ||
| only), which is what lets the background worker sync without a second unlock and | ||
| what makes a browser restart lock the vault again. | ||
|
|
||
| Sync runs when the popup opens, after every local change, and on a timer. Local | ||
| changes are pushed before the pull, so the merge never has to arbitrate. Accounts | ||
| deleted on the server move to a local trash and are purged after the retention | ||
| window rather than being dropped on the spot. | ||
|
|
||
| ## Commands | ||
|
|
||
| ```bash | ||
| bun run dev # chrome, watch mode | ||
| bun run dev:firefox | ||
| bun run build # both targets into dist/ | ||
| bun run package # release/*.zip, store-ready | ||
| bun run icons # regenerate public/icons from scripts/generate-icons.ts | ||
| bun test | ||
| bun run typecheck | ||
| ``` | ||
|
|
||
| `EXT_API_BASE_URL` (or `--api-base-url=`) overrides the compiled-in default and | ||
| the manifest's `host_permissions` entry. | ||
|
|
||
| ## Loading a development build | ||
|
|
||
| - Chrome: `chrome://extensions` → Developer mode → Load unpacked → `dist/chrome` | ||
| - Firefox: `about:debugging#/runtime/this-firefox` → Load Temporary Add-on → | ||
| `dist/firefox/manifest.json` | ||
|
|
||
| ## Releasing | ||
|
|
||
| Bump `version` in `package.json` and merge to `main`. `release-extension.yml` | ||
| detects the change, builds both targets plus the source archive, and publishes a | ||
| GitHub release tagged `ext-v<version>`. Uploading to the two stores is manual. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # Build instructions (add-on reviewers) | ||
|
|
||
| The submitted package is produced by Vite from the TypeScript sources in this | ||
| archive. The archive is the full repository tree at the released commit; only | ||
| `apps/extension` and the workspace packages it imports (`packages/ui`, | ||
| `packages/schemas`, `packages/typescript-config`) take part in this build. | ||
|
|
||
| ## Environment | ||
|
|
||
| | Tool | Version | | ||
| | --- | --- | | ||
| | Bun | 1.3.3 | | ||
| | Operating system | any; the release is built on `ubuntu-latest` | | ||
|
|
||
| Bun is the only prerequisite — it provides the package manager, the runtime for | ||
| the build script, and the test runner. Install it from https://bun.sh. | ||
|
|
||
| ## Steps | ||
|
|
||
| ```bash | ||
| bun install --ignore-scripts | ||
| cd apps/extension | ||
| bun run build:firefox | ||
| ``` | ||
|
|
||
| `--ignore-scripts` skips an unrelated repository postinstall hook (it downloads a | ||
| LaTeX toolchain used by a different app in the monorepo) and has no effect on | ||
| this extension. | ||
|
|
||
| The output appears in `apps/extension/dist/firefox` and is byte-for-byte what | ||
| the uploaded package contains: | ||
|
|
||
| - `manifest.json` — generated by `scripts/manifest.ts` | ||
| - `background.js` — single IIFE bundle of `src/background/index.ts` | ||
| - `popup.html`, `options.html`, `assets/*` — Vite build of the two pages | ||
| - `icons/*.png` — generated by `scripts/generate-icons.ts`, no binary assets are | ||
| checked in | ||
|
|
||
| Replace `build:firefox` with `build:chrome` for the Chrome package; the two | ||
| differ only in the generated manifest. | ||
|
|
||
| ## Verifying | ||
|
|
||
| ```bash | ||
| cd apps/extension | ||
| bun test # includes the RFC 6238 vectors and the sync merge rules | ||
| bun run typecheck | ||
| ``` | ||
|
|
||
| ## Third-party code | ||
|
|
||
| All dependencies are resolved from the public npm registry and pinned by | ||
| `bun.lock` at the repository root. The runtime dependencies are: `react`, | ||
| `react-dom`, `otpauth` (TOTP), `webextension-polyfill`, `radix-ui`, | ||
| `lucide-react`, `clsx`, `tailwind-merge`, `class-variance-authority`, `sonner`. | ||
| No code is fetched at runtime; the manifest's CSP is `script-src 'self'`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| [test] | ||
| preload = ["./test-setup.ts"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>Authenticator Settings</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/pages/options/main.tsx"></script> | ||
| </body> | ||
| </html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: denizlg24/denizlg24.com
Length of output: 19569
🌐 Web query:
actions/checkout persist-credentials default true commit GITHUB_TOKEN contents write permissions💡 Result:
The actions/checkout action has a persist-credentials input that defaults to true [1][2]. When set to true, the authentication token (such as the GITHUB_TOKEN) used to check out the repository is persisted in the local Git configuration [1][3]. This allows subsequent steps in the same job to run authenticated Git commands without needing to manually re-authenticate [1][4]. The token is removed automatically during the post-job cleanup [1][3]. Regarding GITHUB_TOKEN permissions, while actions/checkout defaults to persisting these credentials [1][2], the actual capabilities available to those credentials depend on the permissions explicitly granted to the GITHUB_TOKEN within your workflow file [5][6]. The default GITHUB_TOKEN permissions for GitHub Actions workflows include only read access for repository contents (contents: read) [6]. To enable write operations (such as pushing code or creating branches), you must explicitly grant write permissions in your workflow configuration [6]: permissions: contents: write If you require a job to perform write actions, it is a recommended security practice to use the principle of least privilege by explicitly setting the required permissions in your workflow file rather than relying on default or broad access [5][6]. If your workflow only needs to clone or fetch the repository, keeping the default contents: read is sufficient [6]. To prevent the GITHUB_TOKEN from being persisted in the local Git config for security reasons, you can set persist-credentials to false in the checkout step [1][7].
Citations:
persist-credentialsor change the default tofalseactions/checkout#485Security Misconfiguration (CWE-250)
Scope
contents: writeto the release job and disable credential persistence in build steps.The workflow-level
contents: writegrant also reachesprepareandbuild, whose checkouts persist the token by default. Thebuildjob already executesbuncommands and repo scripts while that token is available, even without installing extra packages. Set the workflow default tocontents: read, givereleasejob-level write permissions, and addpersist-credentials: falseto the checkouts that only need repository access.🧰 Tools
🪛 zizmor (1.28.0)
[error] 12-12: overly broad permissions (excessive-permissions): contents: write is overly broad at the workflow level
(excessive-permissions)
[warning] 12-12: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment
(undocumented-permissions)
🤖 Prompt for AI Agents
Source: Linters/SAST tools