Self-hosting Chromium extensions
Google Chrome and Microsoft Edge provide a store to publish extensions. Extensions published on the store are validated by testers, so you know the quality is ok and the extension does what the description said. However, there is no staging environment before publishing a new version. Also, you cannot publish personal extensions not testable by other people. What you can do is to create a private store. This is not that hard, but it requires some work. In this post, I describe how to self-host extensions for chromium-based browsers such as Google Chrome or Microsoft Edge at no cost.
For security reasons, you cannot install extensions from outside the store without manual configuration. Also, the website serving the extension must use the correct MIME type for the extension file. Indeed, extensions can be harmful, so the browser does not want to install them without your explicit consent.
#Get a domain name
First, you'll need a domain name. It can be a free domain name or a domain name that you own. For this sample, I'll use a free Azure Static Web App. This app comes with a generated domain such as brown-roller-45dsfs2f457.azurestaticapps.net
. It will also provide hosting for static pages, so we'll be able to host our Chromium extension.
Follow this tutorial to create the application and get the domain name: Quickstart: Building your first static site with Azure Static Web Apps
#Deploy the extension
##Repository structure
The repository looks like this:
# GitHub Action file to deploy the website to Azure Static Web App
# This file is generated when you create the App on Azure
.github/workflows/azure-static-web-apps.yml
# Extension source code
src/manifest.json
src/icon.png
src/background.js
sample.extension.pem
The files that are deployed to the website are in the dist
folder:
dist/index.html
dist/manisfest.json
dist/extension.crx
dist/staticwebapp.config.json
##Repository content
Create a manifest file src/manifest.json
, and replace the update_url
with your actual domain:
{
"manifest_version": 3,
"name": "sample-extension",
"version": "1.0.0",
"description": "My sample extension",
"icons": {
"128": "icon.png"
},
"update_url": "https://brown-roller-45dsfs2f457.azurestaticapps.net/manifest.xml",
"background": {
"service_worker": "background.js",
"type": "module"
}
}
Generate the PEM key to sign the extension. This file allows to preserve the same extension id when you publish a new version of the extension. This is important to allow the browser to automatically update the extension.
openssl genrsa -out sample-extension.private.pem 2048
openssl pkcs8 -topk8 -nocrypt -in sample-extension.private.pem -out sample-extension.pem
Pack the extension using chrome.exe
:
# Paths to "src" and the pem file must be absolute
$src = Resolve-Path src
$pem = Resolve-Path sample-extension.pem
& "${env:ProgramFiles}\Google\Chrome\Application\chrome.exe" --pack-extension=$src --pack-extension-key=$pem
Move the generated crx
file to the dist
folder:
Move-Item -Path $src\..\src.crx -Destination dist\sample-extension.crx
Extensions can only be installed by clicking on a link. So, you need to create a web page with a link to download the extension. Create a new file named dist/index.html
. You can do something fancy, or just a simple page with a link to the extension:
<a href="sample-extension.crx">sample-extension.crx</a>
To allow the browser to automatically update the extension, we need to create a manifest file. Create a file dist/manifest.xml
. This file contains a list of app
. Each app
represents an extension. You need to provide the extension id, the latest version, and a link to the extension file. To get the extension id, you can open about://extensions
, enable the developer mode, and drop the .crx
file on the page to install the extension. Then, it should show the extension id in the list.
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='cclehcjioikemgdocpmhpohbakhmhbbc'>
<!-- The version attribute represent the latest version available for the extension. -->
<!-- The browser use this value to auto-update the extension. -->
<updatecheck codebase='https://brown-roller-45dsfs2f457.azurestaticapps.net/sample-extension.crx'
version='1.0.0' />
</app>
</gupdate>
Chrome has strict requirements to allow downloading an extension. One of them is that the extension file must be served with the correct MIME type. You can use the staticwebapp.config.json
file to instruct Azure Static Web App which MIME type it must use for the .crx
file.
{
"mimeTypes": {
".crx": "application/x-chrome-extension"
}
}
You can now deploy the website to Azure Static Web App. If you have created the Azure SWA and associated the GitHub repository, you should already have a workflow file and the secrets should be configured. If so, you should simply need to update the app_location
property in the file:
name: Azure Static Web Apps CI/CD
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload"
app_location: "/dist"
api_location: ""
output_location: ""
skip_app_build: true
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
action: "close"
#Configure the local machine and install the extension
First, you need to configure the local machine to allow extensions from your domain:
Microsoft Edge
PowerShellNew-Item -Path HKLM:\SOFTWARE\Policies\Microsoft\Edge -Name ExtensionInstallSources -Force New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallSources -Name "2" -Value "brown-roller-45dsfs2f457.azurestaticapps.net/*" -Force New-Item -Path HKLM:\SOFTWARE\Policies\Microsoft\Edge -Name ExtensionInstallAllowlist -Force New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallAllowlist -Name "1" -Value "cclehcjioikemgdocpmhpohbakhmhbbc" -Force
Google Chrome
PowerShellNew-Item -Path HKLM:\SOFTWARE\Policies\Google\Chrome -Name ExtensionInstallSources -Force New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallSources -Name "2" -Value "brown-roller-45dsfs2f457.azurestaticapps.net/*" -Force New-Item -Path HKLM:\SOFTWARE\Policies\Google\Chrome -Name ExtensionInstallAllowlist -Force New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallAllowlist -Name "1" -Value "cclehcjioikemgdocpmhpohbakhmhbbc" -Force
If you are on Linux or Mac, you need to create a json file to configure the browser as indicated in the documentation
You can validate the configuration by opening the browser and navigating to about://policy
. If the configuration is not correct, click on the "Reload Policies" button.
You can now navigate to the website and install the extension!
You can also install the extension using a registry key ExtensionInstallForcelist
. This policy specifies a list of apps and extensions that install silently, without user interaction. Users can't uninstall or turn off this setting. Permissions are granted implicitly.
#Additional resources
Do you have a question or a suggestion about this post? Contact me!