pax_global_header00006660000000000000000000000064152517635340014525gustar00rootroot0000000000000052 comment=d5a8966ddefe2e40b5e940ebaff6ea9e48ff34b1 linux-entra-sso-1.11.0/000077500000000000000000000000001525176353400146555ustar00rootroot00000000000000linux-entra-sso-1.11.0/.github/000077500000000000000000000000001525176353400162155ustar00rootroot00000000000000linux-entra-sso-1.11.0/.github/CODEOWNERS000066400000000000000000000001701525176353400176060ustar00rootroot00000000000000# SPDX-FileCopyrightText: Copyright 2026 Siemens AG # SPDX-License-Identifier: MPL-2.0 * @fmoessbauer @jan-kiszka linux-entra-sso-1.11.0/.github/workflows/000077500000000000000000000000001525176353400202525ustar00rootroot00000000000000linux-entra-sso-1.11.0/.github/workflows/build.yml000066400000000000000000000063031525176353400220760ustar00rootroot00000000000000# SPDX-FileCopyrightText: Copyright 2024 Siemens AG # # SPDX-License-Identifier: MPL-2.0 name: build browser extension on: push: branches: - main - rel-* pull_request: workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to checkout the repository permissions: contents: read env: WEB_EXT_VERS: 10.6.0 jobs: reuse-and-codestyle: runs-on: ubuntu-24.04 steps: - name: checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.14' - name: install dependencies run: | sudo apt-get update && sudo apt-get install -y python3-gi pylint python3-pydbus black pip3 install --break-system-packages fsfe-reuse git clean -f -d - name: execute linters run: | reuse lint pylint linux-entra-sso.py - name: check Python code formatting run: black --diff --check . - name: Run biome formatter uses: docker://ghcr.io/biomejs/biome:2.4.16@sha256:e43695cefaf3d3dc0713af731ef3ef150fef5a6bf5aebeb0ee3ffbaffd2ddc5e with: args: ci unit-tests: runs-on: ubuntu-24.04 steps: - name: checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: run unit tests run: make test build-xpi: runs-on: ubuntu-24.04 permissions: id-token: write attestations: write steps: - name: checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: install dependencies run: | sudo apt update && sudo apt install -y make git zip - run: make package deb - name: "web-ext lint Firefox" run: | npx web-ext@${{ env.WEB_EXT_VERS }} lint \ --source-dir build/firefox \ --self-hosted \ --warnings-as-errors - name: "web-ext lint Thunderbird" run: | npx web-ext@${{ env.WEB_EXT_VERS }} lint \ --source-dir build/thunderbird \ --self-hosted \ --warnings-as-errors - name: upload Firefox and Thunderbird extension uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: mozilla-xpi path: | build/**/*.xpi - name: upload chrome extension zip uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: chrome-zip path: | build/chrome/ - name: upload debian package uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: debian-package path: | pkgs/linux-entra-sso_*.deb - name: attest extension artifacts uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 if: github.event_name == 'push' with: subject-path: | build/Linux-Entra-SSO-v* pkgs/linux-entra-sso_*.deb linux-entra-sso-1.11.0/.github/workflows/deploy-update-manifest.yml000066400000000000000000000017551525176353400253650ustar00rootroot00000000000000# SPDX-FileCopyrightText: Copyright 2024 Siemens AG # # SPDX-License-Identifier: MPL-2.0 name: Deploy Firefox Addon Update Manifest on: push: branches: ["main"] workflow_dispatch: permissions: contents: read concurrency: group: "pages" cancel-in-progress: false jobs: deploy: permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 - name: Upload artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: path: '.pages/' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 linux-entra-sso-1.11.0/.github/workflows/release-chrome.yml000066400000000000000000000055271525176353400237010ustar00rootroot00000000000000# SPDX-FileCopyrightText: Copyright 2024 Siemens AG # # SPDX-License-Identifier: MPL-2.0 name: release Chrome extension on: push: tags: - 'v*.*' - 'v*.*.*' permissions: {} env: CRX3_VERS: 2.0.0 CWS_UPLOAD_VERS: 6.0.0 jobs: release-chrome: permissions: contents: read runs-on: ubuntu-24.04 steps: - name: checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: install dependencies run: | sudo apt-get update && sudo apt-get install -y make git zip - name: build packages run: RELEASE_TAG=${{ github.ref_name }} make package # sign the Chrome extension with our own private key, as the # Chrome Web Store upload requires a packed and signed CRX file - name: pack signed Chrome extension env: CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }} run: | KEY_FILE="$(mktemp)" trap 'shred -u "$KEY_FILE"' EXIT printf '%s' "$CRX_PRIVATE_KEY" > "$KEY_FILE" cat "build/Linux-Entra-SSO-${{ github.ref_name }}.chrome.zip" | npx crx3@${{ env.CRX3_VERS }} \ --key "$KEY_FILE" \ --crx "build/Linux-Entra-SSO-${{ github.ref_name }}.chrome.crx" # mnao305/chrome-extension-upload does not support uploading .crx files, # so use the chrome-webstore-upload library directly. # https://github.com/mnao305/chrome-extension-upload/issues/32 # The extension is only uploaded, not published. - name: release Chrome extension on CWS env: CRX_FILE: build/Linux-Entra-SSO-${{ github.ref_name }}.chrome.crx CWS_EXTENSION_ID: jlnfnnolkbjieggibinobhkjdfbpcohn CWS_PUBLISHER_ID: ${{ secrets.CWS_PUBLISHER_ID }} CWS_CLIENT_ID: ${{ secrets.CWS_CLIENT_ID }} CWS_CLIENT_SECRET: ${{ secrets.CWS_CLIENT_SECRET }} CWS_REFRESH_TOKEN: ${{ secrets.CWS_REFRESH_TOKEN }} run: | npm install --no-save chrome-webstore-upload@${{ env.CWS_UPLOAD_VERS }} node --input-type=module <<'EOF' import chromeWebstoreUpload from 'chrome-webstore-upload'; const store = chromeWebstoreUpload({ publisherId: process.env.CWS_PUBLISHER_ID, extensionId: process.env.CWS_EXTENSION_ID, clientId: process.env.CWS_CLIENT_ID, clientSecret: process.env.CWS_CLIENT_SECRET, refreshToken: process.env.CWS_REFRESH_TOKEN, }); const token = await store.fetchToken(); const response = await store.uploadExisting(process.env.CRX_FILE, token); console.log('Upload response:', JSON.stringify(response, null, 2)); if (response.uploadState === 'FAILURE') { console.error('Chrome Web Store upload failed'); process.exit(1); } EOF linux-entra-sso-1.11.0/.github/workflows/release-mozilla.yml000066400000000000000000000105221525176353400240620ustar00rootroot00000000000000# SPDX-FileCopyrightText: Copyright 2024 Siemens AG # # SPDX-License-Identifier: MPL-2.0 name: release Mozilla extensions on: push: tags: - 'v*.*' - 'v*.*.*' permissions: {} env: WEB_EXT_VERS: 10.6.0 jobs: release-mozilla: permissions: contents: write pull-requests: write id-token: write attestations: write runs-on: ubuntu-24.04 steps: - name: checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: get git committer run: | echo "GIT_COMMITTER=$(git show -s --format='%cn <%ce>')" >> $GITHUB_ENV - name: install dependencies run: | sudo apt-get update && sudo apt-get install -y make git zip - name: build packages run: RELEASE_TAG=${{ github.ref_name }} make package deb - name: sign Firefox extension by Mozilla run: | npx web-ext@${{ env.WEB_EXT_VERS }} sign \ --channel listed \ --approval-timeout 900000 \ --api-key ${{ secrets.AMO_API_KEY }} \ --api-secret ${{ secrets.AMO_API_SECRET }} \ --source-dir build/firefox \ --artifacts-dir build \ --amo-metadata platform/firefox/amo-metadata.json # self distributed extensions on Thunderbird are not signed - name: build Thunderbird extension run: | npx web-ext@${{ env.WEB_EXT_VERS }} build \ --source-dir build/thunderbird \ --artifacts-dir build \ --filename '{name}-{version}.thunderbird.xpi' - name: upload firefox extension uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: firefox-signed-xpi path: | build/linux_entra_sso-*.xpi - name: upload debian package uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: debian-package path: | pkgs/linux-entra-sso_*.deb - name: attest Firefox extension build uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-path: | build/linux_entra_sso-*.xpi pkgs/linux-entra-sso_*.deb - name: create release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: files: | build/linux_entra_sso-*.xpi pkgs/linux-entra-sso_*.deb token: ${{ secrets.GITHUB_TOKEN }} body: | Release of version ${{ github.ref_name }} tag_name: ${{ github.ref_name }} draft: false prerelease: true - name: checkout main branch for update manifests uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: main - name: create update manifest for Thunderbird run: | VERSION=$(echo ${{ github.ref_name }} | cut -c 2-) DIGEST="sha256:$(sha256sum build/linux_entra_sso-${VERSION}.thunderbird.xpi | cut -d ' ' -f 1)" LINK="https://github.com/siemens/linux-entra-sso/releases/download/v${VERSION}/linux_entra_sso-${VERSION}.thunderbird.xpi" jq --arg version "${VERSION}" --arg digest "${DIGEST}" --arg link "${LINK}" \ '."addons"."@linux-entra-sso.tb"."updates" += [{"version":$version, "update_link":$link, "update_hash":$digest}]' \ .pages/thunderbird/updates.json \ > .pages/thunderbird/updates.json.tmp && mv .pages/thunderbird/updates.json.tmp .pages/thunderbird/updates.json - name: prepare PR for Thunderbird update manifests uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: add-paths: | .pages/thunderbird/updates.json commit-message: "chore: release Thunderbird update manifests" branch: ci/release-firefox-update-manifest base: main title: "chore: release Thunderbird update manifests [bot]" assignees: fmoessbauer reviewers: jan-kiszka author: ${{ env.GIT_COMMITTER }} committer: ${{ env.GIT_COMMITTER }} signoff: true body: | Publish update manifest for Thunderbird extensions, version ${{ github.ref_name }}. linux-entra-sso-1.11.0/.gitignore000066400000000000000000000002241525176353400166430ustar00rootroot00000000000000# SPDX-FileCopyrightText: Copyright 2024 Siemens AG # # SPDX-License-Identifier: MPL-2.0 Linux-Entra-SSO*.xpi __pycache__/ build/ debuild.d/ pkgs/ linux-entra-sso-1.11.0/.pages/000077500000000000000000000000001525176353400160325ustar00rootroot00000000000000linux-entra-sso-1.11.0/.pages/firefox/000077500000000000000000000000001525176353400174745ustar00rootroot00000000000000linux-entra-sso-1.11.0/.pages/firefox/updates.json000066400000000000000000000136511525176353400220420ustar00rootroot00000000000000{ "addons": { "linux-entra-sso@example.com": { "updates": [ { "version": "0.6.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v0.6.1/linux_entra_sso-0.6.1.xpi", "update_hash": "sha256:edb6d8b754d4c2390517815ed72f5a96f1394f322a555cf54f477ca5190562c1" }, { "version": "0.7", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v0.7/linux_entra_sso-0.7.xpi", "update_hash": "sha256:52cf7b7156dbcf28b7e3618fed0d904d6d2d422314ba252993a421e5ffcc165f" }, { "version": "0.8", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v0.8/linux_entra_sso-0.8.xpi", "update_hash": "sha256:fec76078c4bd89505cdaef8dc039ec2b2af8ee2e8a097d0c0e0d3983fbc5f9ce" }, { "version": "0.9", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v0.9/linux_entra_sso-0.9.xpi", "update_hash": "sha256:dc44b695a83c0b8adf92f0fe3c60aa1c01dc0df9e521e262bdf2c7a576720d50" }, { "version": "1.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.0/linux_entra_sso-1.0.xpi", "update_hash": "sha256:6f57198cd5dde85967dc7c5a3a27d421f66e3acb74a7bd3ea5a1cdaf65976b04" }, { "version": "1.1.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.1.0/linux_entra_sso-1.1.0.xpi", "update_hash": "sha256:db046d1c0f59990be46111d03d5803c477f45e3a70df93fe68edd72fe54cd303" }, { "version": "1.2.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.2.0/linux_entra_sso-1.2.0.xpi", "update_hash": "sha256:33350336b7f049ff38065050b6f91563ecaff331e0c31f60796162a4ae56c156" }, { "version": "1.3.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.3.0/linux_entra_sso-1.3.0.xpi", "update_hash": "sha256:29168cdef8001059c996d918e7fb6c27e3a1d569802a6b4f1bb96221a947ced5" }, { "version": "1.3.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.3.1/linux_entra_sso-1.3.1.xpi", "update_hash": "sha256:4d920d20d39292e7335ffdfbc04603b0d286eb3de980699fa4c1648104b543fb" }, { "version": "1.4.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.4.0/linux_entra_sso-1.4.0.xpi", "update_hash": "sha256:5575d57b7c002cb02286754fe4855a15dda608f5c46026e68c1609fb220e1df1" }, { "version": "1.5.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.5.0/linux_entra_sso-1.5.0.xpi", "update_hash": "sha256:9efa88c1978b6aca3766c5e6dba923f9dd3f89f1a43326723ebc4103d5cf318e" }, { "version": "1.5.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.5.1/linux_entra_sso-1.5.1.xpi", "update_hash": "sha256:7e2d38a08a6da22e63340a284e76e9a6bcba8fd74c81a576e52952cd24372ca8" }, { "version": "1.6.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.6.0/linux_entra_sso-1.6.0.xpi", "update_hash": "sha256:9e18e4aa5482ca128f7229b0699cbe0c0f0d91454718e9656d7433a69c99fe09" }, { "version": "1.7.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.7.0/linux_entra_sso-1.7.0.xpi", "update_hash": "sha256:d425866f65b5fc70082cb8952e9f9b806914fc88a4d0ccaa89ad19c5b1f59c60" }, { "version": "1.7.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.7.1/linux_entra_sso-1.7.1.xpi", "update_hash": "sha256:a1ad90f08b25e3bf732405647379e06e15262b77cefd24d46a8936f4ff69e84e" }, { "version": "1.7.2", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.7.2/linux_entra_sso-1.7.2.xpi", "update_hash": "sha256:440d7ffcf52ed3fb78593abfae2e8ad568ae01902da659213868e27a789989e0" }, { "version": "1.8.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.8.0/linux_entra_sso-1.8.0.xpi", "update_hash": "sha256:058588747896f6d25851c7fa8b56847ba6a4683c7c6bf2fbae08b2fefecef769" }, { "version": "1.8.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.8.1/linux_entra_sso-1.8.1.xpi", "update_hash": "sha256:35f0368d45b39de19dfdb07d3345b6cd65ce1819b1c6ece8ffb1deae2027529d" }, { "version": "1.9.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.9.0/linux_entra_sso-1.9.0.xpi", "update_hash": "sha256:e86c2d28861dc4544feeb460184908803a582bd8e255bef26de8debdfd8c4b34" }, { "version": "1.9.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.9.1/linux_entra_sso-1.9.1.xpi", "update_hash": "sha256:8d21d637296612040d155c3d35a8ae9fe31bad8b1d78fdd333cef3c591281286" }, { "version": "1.10.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.10.0/linux_entra_sso-1.10.0.xpi", "update_hash": "sha256:28955527c51cd6e1c212710b86c122e8d13d1c011736bf0360d8af072e83d510" }, { "version": "1.10.2", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.10.2/linux_entra_sso-1.10.2.xpi", "update_hash": "sha256:55278d1aa94d7078ac126ff7ddf41a9714c264ab95814d9fa6961d0a4d67d51f" } ] } } } linux-entra-sso-1.11.0/.pages/firefox/updates.json.license000066400000000000000000000001241525176353400234520ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/.pages/thunderbird/000077500000000000000000000000001525176353400203445ustar00rootroot00000000000000linux-entra-sso-1.11.0/.pages/thunderbird/updates.json000066400000000000000000000066511525176353400227140ustar00rootroot00000000000000{ "addons": { "@linux-entra-sso.tb": { "updates": [ { "version": "1.5.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.5.0/linux_entra_sso-1.5.0.thunderbird.xpi", "update_hash": "sha256:d0e47ead833030a178a33934b634ed929b94faf265a2d1a63e51f6a02c61df49" }, { "version": "1.5.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.5.1/linux_entra_sso-1.5.1.thunderbird.xpi", "update_hash": "sha256:3f145d3bebaef978acf4629f70b5ee646069fdc7d838499f2cf1c4ebab7d42fe" }, { "version": "1.6.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.6.0/linux_entra_sso-1.6.0.thunderbird.xpi", "update_hash": "sha256:8121ef5745bf67f44694d4a52d8cc70f5ea6c4c9ba2eb0c32f28d399d0c35733" }, { "version": "1.7.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.7.0/linux_entra_sso-1.7.0.thunderbird.xpi", "update_hash": "sha256:1d19712d85cfeb0765936e285388073a097d4b8b3fcdef711152b2d4d0c09fce" }, { "version": "1.7.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.7.1/linux_entra_sso-1.7.1.thunderbird.xpi", "update_hash": "sha256:53b500deedaaac018260f6e78957e6dd3eca2750b6e335d2632d3817c5c72cb4" }, { "version": "1.7.2", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.7.2/linux_entra_sso-1.7.2.thunderbird.xpi", "update_hash": "sha256:e4b63a7ad8ce010c0f1944e8d836b632c81de7ec424a96eacf30a6569f37722b" }, { "version": "1.8.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.8.0/linux_entra_sso-1.8.0.thunderbird.xpi", "update_hash": "sha256:dd627c9afa5ea56727c6d587f63baec24f8aaef6dd57962ea6228a24c68c1417" }, { "version": "1.8.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.8.1/linux_entra_sso-1.8.1.thunderbird.xpi", "update_hash": "sha256:86eae652e855ce5e9cd9d9a6a2d2e50148546e53badb4eca1422bd5575fea24f" }, { "version": "1.9.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.9.0/linux_entra_sso-1.9.0.thunderbird.xpi", "update_hash": "sha256:f858ada597db0f0668e4d7a3265feaa0cac40ef706c0817cae8e49c349b9891a" }, { "version": "1.9.1", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.9.1/linux_entra_sso-1.9.1.thunderbird.xpi", "update_hash": "sha256:379077c0acd1448cdbd90de84160750d7f54cdb24276d48d6088325658df2eb3" }, { "version": "1.10.0", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.10.0/linux_entra_sso-1.10.0.thunderbird.xpi", "update_hash": "sha256:3958ba97c7e093f575e6c9ad8034081163d01b89313983851f8ada797b698d00" }, { "version": "1.10.2", "update_link": "https://github.com/siemens/linux-entra-sso/releases/download/v1.10.2/linux_entra_sso-1.10.2.thunderbird.xpi", "update_hash": "sha256:49e4574ddc5da368e46ba953fcfdde981ccfe219619b49bbc769a729613efb4b" } ] } } } linux-entra-sso-1.11.0/.pages/thunderbird/updates.json.license000066400000000000000000000001211525176353400243170ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2025 Siemens SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/CONTRIBUTING.md000066400000000000000000000101451525176353400171070ustar00rootroot00000000000000 # Contributing to linux-entra-sso Contributions are always welcome. This document explains the general requirements on contributions and the recommended preparation steps. ## Contribution Checklist - use git to manage your changes [*recommended*] - follow Python coding style outlined in pep8 [**required**] - add signed-off to all patches [**required**] - to certify the "Developer's Certificate of Origin", see below - check with your employer when not working on your own! - post follow-up version(s) if feedback requires this - send reminder if nothing happened after about a week - when adding new files, add a license header (see existing files) [**required**] Developer's Certificate of Origin 1.1 ------------------------------------- When signing-off a patch for this project like this Signed-off-by: Random J Developer using your real name (no pseudonyms or anonymous contributions), you declare the following: By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ## Testing Please test the extension on all supported platforms (browsers). If you cannot test on a platform (e.g., because you don't have it), clearly state this. We also provide a mock implementation of the backend part, which can be installed using `make local-install-mock`. This mock processes and returns syntactically valid data via the native messaging protocol, enabling testing of features like multi-account support that are otherwise difficult to test. It does not require a `microsoft-identity-broker` to be running but also does not issue valid tokens. ## Maintainers: Create Releases The creation of public releases is a partially automated process: 1. update code and create release tags: `VERSION= make release` 2. push to GitHub: `git push origin main && git push origin v` 3. wait for release action to finish (public release is created as draft) 4. manually inspect signed xpi (double check) 5. publish CWS upload (answer questions on permission changes) 6. wait for CWS to review and sign extension, upload `.crx` to releases page 7. add release-notes to public release and release (immutable!) 8. merge auto-created MR to enroll Firefox update manifest When creating a bugfix release from a dedicated release branch, extra steps are needed: 1. ensure patches have been merged to main 2. checkout base tag: `git checkout v` 3. create release branch: `git checkout -b rel-.y` 4. ensure all needed CI changes are cherry-picked 5. follow normal release creation process When creating a security fix release, further ensure: 1. prepare patches on private fork, for review by reporter, add either CVE or GHSA as `Fixes:` tag 2. do not merge from private fork, as this does not run the CI on the patches 3. push patches of private fork to public branch, create MR 4. once merged, create release via normal release process 5. publish advisory linux-entra-sso-1.11.0/LICENSE000066400000000000000000000405271525176353400156720ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. linux-entra-sso-1.11.0/LICENSES/000077500000000000000000000000001525176353400160625ustar00rootroot00000000000000linux-entra-sso-1.11.0/LICENSES/MPL-2.0.txt000066400000000000000000000405271525176353400175600ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. linux-entra-sso-1.11.0/MAINTAINERS.md000066400000000000000000000003371525176353400167540ustar00rootroot00000000000000 # Maintainers - Felix Moessbauer (fmoessbauer) - Jan Kiszka (jan-kiszka) linux-entra-sso-1.11.0/Makefile000066400000000000000000000304341525176353400163210ustar00rootroot00000000000000# # Entra ID SSO via Microsoft Identity Broker on Linux # # SPDX-License-Identifier: MPL-2.0 # SPDX-FileCopyrightText: Copyright (c) Jan Kiszka, 2020-2024 # SPDX-FileCopyrightText: Copyright (c) Siemens AG, 2024 # # Authors: # Jan Kiszka # Felix Moessbauer # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # prefix ?= /usr/local exec_prefix ?= $(prefix) libexecdir ?= $(exec_prefix)/libexec # do not prefix with $(prefix) as these dirs are defined by the browsers firefox_nm_dir ?= /usr/lib/mozilla/native-messaging-hosts chrome_nm_dir ?= /etc/opt/chrome/native-messaging-hosts chrome_ext_dir ?= /usr/share/google-chrome/extensions chromium_nm_dir ?= /etc/chromium/native-messaging-hosts # local install path brave_lconfig_dir ?= $(HOME)/.config/BraveSoftware/Brave-Browser firefox_lconfig_dir ?= $(HOME)/.mozilla chrome_lconfig_dir ?= $(HOME)/.config/google-chrome chromium_lconfig_dir ?= $(HOME)/.config/chromium vivaldi_lconfig_dir ?= $(HOME)/.config/vivaldi # python3 system interpreter for global installs python3_bin ?= $(shell which python3) ifeq ($(V),1) Q = else Q = @ endif PACKAGE_NAME=Linux-Entra-SSO RELEASE_TAG ?= $(shell git describe --match "v[0-9].[0-9]*" --dirty) WEBEXT_VERSION=$(RELEASE_TAG:v%=%) ARCHIVE_NAME=$(PACKAGE_NAME)-$(RELEASE_TAG) COMMON_INPUT_FILES= \ LICENSES/MPL-2.0.txt \ src/account.js \ src/app-state.js \ src/background.js \ src/broker.js \ src/device.js \ src/platform.js \ src/policy.js \ src/state-machine.js \ src/utils.js \ icons/profile-outline_48.png \ icons/profile-outline_48.png.license \ popup/menu.css \ popup/menu.js \ popup/menu.html CHROME_INPUT_FILES= \ $(COMMON_INPUT_FILES) \ platform/chrome/manifest.json \ platform/chrome/manifest.json.license \ platform/chrome/js/platform-chrome.js \ platform/chrome/js/platform-factory.js \ platform/chrome/storage-schema.json \ platform/chrome/storage-schema.json.license \ icons/linux-entra-sso_48.png \ icons/linux-entra-sso_48.png.license \ icons/linux-entra-sso_128.png \ icons/linux-entra-sso_128.png.license FIREFOX_INPUT_FILES= \ $(COMMON_INPUT_FILES) \ platform/firefox/manifest.json \ platform/firefox/manifest.json.license \ platform/firefox/js/platform-firefox.js \ platform/firefox/js/platform-factory.js \ icons/linux-entra-sso.svg \ icons/profile-outline.svg THUNDERBIRD_INPUT_FILES= \ $(COMMON_INPUT_FILES) \ platform/thunderbird/manifest.json \ platform/thunderbird/manifest.json.license \ platform/thunderbird/js/platform-thunderbird.js \ platform/thunderbird/js/platform-factory.js \ icons/linux-entra-sso.svg \ icons/profile-outline.svg # common files for all platforms (relative to build directory) CHROME_PACKAGE_FILES= \ $(COMMON_INPUT_FILES) \ src/platform-chrome.js \ src/platform-factory.js \ manifest.json \ manifest.json.license \ storage-schema.json \ storage-schema.json.license \ icons/linux-entra-sso_48.png \ icons/linux-entra-sso_48.png.license \ icons/linux-entra-sso_128.png \ icons/linux-entra-sso_128.png.license \ popup/profile-outline.svg FIREFOX_PACKAGE_FILES= \ $(COMMON_INPUT_FILES) \ src/platform-firefox.js \ src/platform-factory.js \ manifest.json \ manifest.json.license \ icons/linux-entra-sso.svg \ popup/profile-outline.svg THUNDERBIRD_PACKAGE_FILES= \ $(FIREFOX_PACKAGE_FILES) \ src/platform-thunderbird.js UPDATE_VERSION='s|"version":.*|"version": "$(VERSION)",|' UPDATE_VERSION_PY='s|0.0.0-dev|$(WEBEXT_VERSION)|g' UPDATE_PYTHON_INTERPRETER='1,1s:^\#!.*:\#!$(python3_bin):' CHROME_EXT_ID=$(shell $(CURDIR)/platform/chrome/get-ext-id.py $(CURDIR)/build/chrome/) CHROME_EXT_ID_SIGNED=jlnfnnolkbjieggibinobhkjdfbpcohn # debian package related vars DEBIAN_PV = $(RELEASE_TAG:v%=%) DEBIAN_PN = linux-entra-sso DEBIAN_DESCRIPTION = Entra ID SSO via Microsoft Identity Broker on Linux DEBIAN_DESTDIR := $(CURDIR)/debuild.d DEBIAN_ARCH = all DEBIAN_PKG_DIR = $(CURDIR)/pkgs DEBIAN_PKG_FILE = $(DEBIAN_PKG_DIR)/$(DEBIAN_PN)_$(DEBIAN_PV)_$(DEBIAN_ARCH).deb all package: clean $(CHROME_INPUT_FILES) $(FIREFOX_INPUT_FILES) $(THUNDERBIRD_INPUT_FILES) for P in firefox thunderbird chrome; do \ mkdir -p build/$$P/icons build/$$P/popup; \ cp platform/$$P/manifest* build/$$P; \ cp -rf LICENSES src build/$$P/; \ cp platform/$$P/js/* build/$$P/src; \ done cp -r build/firefox/icons build/firefox/popup build/thunderbird/ cp platform/chrome/storage* build/chrome/ cp icons/*.svg icons/profile-outline_48.* build/firefox/icons/ cp icons/*.png* icons/profile-outline.svg build/chrome/icons/ cp popup/menu.* icons/linux-entra-sso.svg icons/profile-outline.svg build/firefox/popup/ cp popup/menu.* icons/linux-entra-sso.svg icons/profile-outline.svg build/chrome/popup/ # thunderbird is almost identical to Firefox cp -r build/firefox/icons build/firefox/popup build/thunderbird/ cp build/firefox/src/platform-firefox.js build/thunderbird/src/ cd build/firefox && zip -r ../$(ARCHIVE_NAME).firefox.xpi $(FIREFOX_PACKAGE_FILES) cd build/thunderbird && zip -r ../$(ARCHIVE_NAME).thunderbird.xpi $(THUNDERBIRD_PACKAGE_FILES) cd build/chrome && zip -r ../$(ARCHIVE_NAME).chrome.zip $(CHROME_PACKAGE_FILES) deb: install --mode 00755 --directory $(DEBIAN_DESTDIR) $(MAKE) install DESTDIR=$(DEBIAN_DESTDIR) python3_bin=/usr/bin/python3 prefix=/usr install --mode 644 -D --target-directory=$(DEBIAN_DESTDIR)/usr/share/doc/$(DEBIAN_PN) README.md CONTRIBUTING.md MAINTAINERS.md PRIVACY.md LICENSES/MPL-2.0.txt install --mode 00755 --directory $(DEBIAN_DESTDIR)/DEBIAN { \ echo Package: $(DEBIAN_PN); \ echo Architecture: $(DEBIAN_ARCH); \ echo Section: admin; \ echo Priority: optional; \ echo 'Maintainer: Dr. Johann Pfefferl '; \ echo Installed-Size: `du --summarize $(DEBIAN_DESTDIR) | cut --fields=1`; \ echo 'Depends: python3-pydbus, python3-gi'; \ echo Version: $(DEBIAN_PV); \ echo Description: $(DEBIAN_DESCRIPTION); \ } > $(DEBIAN_DESTDIR)/DEBIAN/control install --mode 775 --directory $(DEBIAN_PKG_DIR) dpkg-deb --deb-format=2.0 --root-owner-group --build $(DEBIAN_DESTDIR) $(DEBIAN_PKG_DIR) @echo Package can be found here: $(DEBIAN_PKG_FILE) deb_clean: rm -rf $(DEBIAN_PKG_DIR) $(DEBIAN_DESTDIR) clean: deb_clean rm -rf build test: ${Q}for t in tests/*_test.mjs; do \ echo "running $$t"; \ node $$t || exit 1; \ done release: ${Q}if [ -z "$(VERSION)" ]; then \ echo "VERSION is not set"; \ exit 1; \ fi ${Q}if [ -n "`git status -s -uno`" ]; then \ echo "Working directory is dirty!"; \ exit 1; \ fi ${Q}sed -i $(UPDATE_VERSION) platform/*/manifest.json git commit -s platform/firefox/manifest.json platform/thunderbird/manifest.json platform/chrome/manifest.json -m "Bump version number" git tag -as v$(VERSION) -m "Release v$(VERSION)" ####################### # local install targets ####################### local-install-firefox: install -d $(firefox_lconfig_dir)/native-messaging-hosts install -m 0644 platform/firefox/linux_entra_sso.json $(firefox_lconfig_dir)/native-messaging-hosts ${Q}sed -i 's|/usr/local/lib/linux-entra-sso/|'$(firefox_lconfig_dir)'/|' $(firefox_lconfig_dir)/native-messaging-hosts/linux_entra_sso.json install -m 0755 linux-entra-sso.py $(firefox_lconfig_dir)/linux-entra-sso ${Q}sed -i $(UPDATE_VERSION_PY) $(firefox_lconfig_dir)/linux-entra-sso # Helper target for installing Chromium-based browsers # Usage: make local-install-chromium-based BROWSER_CONFIG_PATH=~/.config/chromium local-install-chromium-based: install -d $(BROWSER_CONFIG_PATH)/NativeMessagingHosts install -m 0644 platform/chrome/linux_entra_sso.json $(BROWSER_CONFIG_PATH)/NativeMessagingHosts ${Q}sed -i 's|/usr/local/lib/linux-entra-sso/|'$(BROWSER_CONFIG_PATH)'/|' $(BROWSER_CONFIG_PATH)/NativeMessagingHosts/linux_entra_sso.json # compute extension id and and grant permission ${Q}sed -i 's|{extension_id}|$(CHROME_EXT_ID)|' $(BROWSER_CONFIG_PATH)/NativeMessagingHosts/linux_entra_sso.json install -m 0755 linux-entra-sso.py $(BROWSER_CONFIG_PATH)/linux-entra-sso ${Q}sed -i $(UPDATE_VERSION_PY) $(BROWSER_CONFIG_PATH)/linux-entra-sso local-install-brave: $(MAKE) local-install-chromium-based BROWSER_CONFIG_PATH=$(brave_lconfig_dir) local-install-chrome: $(MAKE) local-install-chromium-based BROWSER_CONFIG_PATH=$(chrome_lconfig_dir) local-install-chromium: $(MAKE) local-install-chromium-based BROWSER_CONFIG_PATH=$(chromium_lconfig_dir) local-install-vivaldi: $(MAKE) local-install-chromium-based BROWSER_CONFIG_PATH=$(vivaldi_lconfig_dir) local-install: local-install-brave local-install-chrome local-install-chromium local-install-firefox local-install-vivaldi # For testing, we provide a mock implementation of the broker communication local-install-mock: local-install install -m 0755 tests/linux_entra_sso_mock.py $(brave_lconfig_dir)/linux-entra-sso install -m 0755 tests/linux_entra_sso_mock.py $(chrome_lconfig_dir)/linux-entra-sso install -m 0755 tests/linux_entra_sso_mock.py $(chromium_lconfig_dir)/linux-entra-sso install -m 0755 tests/linux_entra_sso_mock.py $(firefox_lconfig_dir)/linux-entra-sso install -m 0755 tests/linux_entra_sso_mock.py $(vivaldi_lconfig_dir)/linux-entra-sso #################################### # system install / uninstall targets #################################### install: ${Q}[ -z "$(python3_bin)" ] && { echo "python3 not found. Please set 'python3_bin'."; exit 1; } || true # Host application install -d $(DESTDIR)/$(libexecdir)/linux-entra-sso install -m 0755 linux-entra-sso.py $(DESTDIR)/$(libexecdir)/linux-entra-sso/linux-entra-sso ${Q}sed -i $(UPDATE_VERSION_PY) $(DESTDIR)/$(libexecdir)/linux-entra-sso/linux-entra-sso ${Q}sed -i $(UPDATE_PYTHON_INTERPRETER) $(DESTDIR)/$(libexecdir)/linux-entra-sso/linux-entra-sso # Firefox install -d $(DESTDIR)/$(firefox_nm_dir) install -m 0644 platform/firefox/linux_entra_sso.json $(DESTDIR)/$(firefox_nm_dir) ${Q}sed -i 's|/usr/local/lib/|'$(libexecdir)/'|' $(DESTDIR)/$(firefox_nm_dir)/linux_entra_sso.json # Chrome install -d $(DESTDIR)/$(chrome_nm_dir) install -m 0644 platform/chrome/linux_entra_sso.json $(DESTDIR)/$(chrome_nm_dir) ${Q}sed -i 's|/usr/local/lib/|'$(libexecdir)/'|' $(DESTDIR)/$(chrome_nm_dir)/linux_entra_sso.json ${Q}sed -i '/{extension_id}/d' $(DESTDIR)/$(chrome_nm_dir)/linux_entra_sso.json install -d $(DESTDIR)/$(chrome_ext_dir) install -m 0644 platform/chrome/extension.json $(DESTDIR)/$(chrome_ext_dir)/$(CHROME_EXT_ID_SIGNED).json # Chromium install -d $(DESTDIR)/$(chromium_nm_dir) install -m 0644 platform/chrome/linux_entra_sso.json $(DESTDIR)/$(chromium_nm_dir) ${Q}sed -i 's|/usr/local/lib/|'$(libexecdir)/'|' $(DESTDIR)/$(chromium_nm_dir)/linux_entra_sso.json ${Q}sed -i '/{extension_id}/d' $(DESTDIR)/$(chromium_nm_dir)/linux_entra_sso.json uninstall: rm -rf $(DESTDIR)/$(libexecdir)/linux-entra-sso rm -f $(DESTDIR)/$(firefox_nm_dir)/linux_entra_sso.json rm -f $(DESTDIR)/$(chrome_nm_dir)/linux_entra_sso.json rm -f $(DESTDIR)/$(chromium_nm_dir)/linux_entra_sso.json rm -f $(DESTDIR)/$(chrome_ext_dir)/$(CHROME_EXT_ID_SIGNED).json ######################### # local uninstall targets ######################### local-uninstall-brave: rm -f $(brave_lconfig_dir)/NativeMessagingHosts/linux_entra_sso.json rm -f $(brave_lconfig_dir)/linux-entra-sso local-uninstall-chrome: rm -f $(chrome_lconfig_dir)/NativeMessagingHosts/linux_entra_sso.json rm -f $(chrome_lconfig_dir)/linux-entra-sso local-uninstall-chromium: rm -f $(chromium_lconfig_dir)/NativeMessagingHosts/linux_entra_sso.json rm -f $(chromium_lconfig_dir)/linux-entra-sso local-uninstall-firefox: rm -f $(firefox_lconfig_dir)/native-messaging-hosts/linux_entra_sso.json rm -f $(firefox_lconfig_dir)/linux-entra-sso local-uninstall-vivaldi: rm -f $(vivaldi_lconfig_dir)/NativeMessagingHosts/linux_entra_sso.json rm -f $(vivaldi_lconfig_dir)/linux-entra-sso local-uninstall: local-uninstall-brave local-uninstall-chrome local-uninstall-chromium local-uninstall-firefox local-uninstall-vivaldi .PHONY: clean release deb deb_clean test .PHONY: local-install-firefox local-install-chrome local-install-brave local-install-chromium-based local-install-vivaldi local-install .PHONY: local-uninstall-firefox local-uninstall-chromium-based local-uninstall-chrome local-uninstall-brave local-uninstall-vivaldi local-uninstall .PHONY: local-install-mock .PHONY: install uninstall linux-entra-sso-1.11.0/PRIVACY.md000066400000000000000000000043751525176353400163250ustar00rootroot00000000000000 # Privacy Policy The `linux-entra-sso` browser extension does not collect any data of any kind. - `linux-entra-sso` has no home server - `linux-entra-sso` doesn't embed any analytic or telemetry hooks in its code To fulfill its purpose, the extension interfaces with the following services: - Microsoft Graph API (web service) - Microsoft Entra ID (web service) - `com.microsoft.identity.broker1` (`broker`, DBus service) ## Microsoft Graph API To show data about the currently logged in user (e.g. the profile picture in the app icon), we request an access token for the `graph.microsoft.com` API. The token is acquired from the locally running broker. ## Microsoft Identity Broker DBus service (broker) To implement the SSO functionality, a `PRT SSO Cookie` is requested from the locally running `com.microsoft.identity.broker1` DBus service. In the Firefox version, whenever an URL starting with `https://login.microsoftonline.com/` (Entra ID login URL) is accessed, a token is requested with the full request URL. On Chrome and Chromium, the `PRT SSO Cookie` is requested periodically with a generic URL. The returned token is injected into all http requests hitting the Entra ID login URL. ### Note on required and optional host permissions We use the `WebRequest` (Firefox) or `declarativeNetRequest` (Chrome) API to inject the `PRT SSO Cookie` into requests targeting the login provider. To support this, we need the permission to access your data on `https://login.microsoftonline.com/`. This permission is (usually) requested at extension install time (required permission). For single-page applications (SPAs, like the Teams PWA) that perform automated token refreshes in the background, we further need the permission to access your data on the corresponding domains. To minimize the number of permissions we request, we provide users with the ability to grant these permissions on a case-by-case basis via the extension's UI or policy settings. Granted permissions can also be revoked through the same interface. ## Privacy statement for Microsoft services The privacy statement for all Microsoft provided services is found on . linux-entra-sso-1.11.0/README.md000066400000000000000000000152641525176353400161440ustar00rootroot00000000000000 # Entra ID SSO via Microsoft Identity Broker on Linux This browser extension uses a locally running Microsoft Identity Broker to authenticate the current user on Microsoft Entra ID on Linux devices. By that, also sites behind conditional access policies can be accessed. The extension is written for Firefox but provides a limited support for Google Chrome, Chromium and Thunderbird. > [!NOTE] > This extension will only work on intune-enabled Linux devices. Please double > check this by running the `intune-portal` application and check if your user > is logged in (after clicking `sign-in`). ## Installation The extension consists of two parts: - a host program that communicates with the Microsoft Identity Broker via DBus - a WebExtension that injects the acquired tokens into the corresponding requests ### Dependencies The extension requires [PyGObject](https://pygobject.gnome.org/) and [pydbus](https://github.com/LEW21/pydbus) as runtime dependencies. - On Debian: `sudo apt-get install python3-gi python3-pydbus` - On Arch Linux: `sudo pacman -S python-gobject python-pydbus` - If you are using a Python version manager such as `asdf` you must install the Python packages manually: `pip install PyGObject pydbus` ### Installation of Host Tooling 1. Clone this repository: ```bash $ git clone https://github.com/siemens/linux-entra-sso.git $ cd linux-entra-sso ``` 2. Run the local install command (for the intended target): ```bash $ # Firefox & Thunderbird $ make local-install-firefox $ # Chromium, Chrome and Brave $ make local-install-(brave|chrome|chromium|vivaldi) $ # All supported browsers $ make local-install ``` > [!NOTE] > System-wide installation and configuration is supported. For more information, see [Global Install](docs/global_install.md). ### Installation of WebExtension To complete the setup, install the WebExtension in your browser. This is necessary alongside the host tooling for the extension to function properly. **Firefox & Thunderbird: Signed Version from GitHub Releases**: Install the signed webextension `linux_entra_sso-.xpi` from the [project's releases page](https://github.com/siemens/linux-entra-sso/releases). If you are installing for Thunderbird, right-click the link and select "Save Link As..." to avoid installing it in Firefox. **Chromium, Chrome & Brave: Signed Extension from Chrome Web Store**: Install the signed browser extension from the [Chrome Web Store](https://chrome.google.com/webstore/detail/jlnfnnolkbjieggibinobhkjdfbpcohn). **Development Version and Other Browsers**: If you want to execute unsigned versions of the extension (e.g. test builds) on Firefox, you have to use either Firefox ESR, nightly or developer, as [standard Firefox does not allow installing unsigned extensions](https://support.mozilla.org/en-US/kb/add-on-signing-in-firefox#w_what-are-my-options-if-i-want-to-use-an-unsigned-add-on-advanced-users) since version 48. To build the extension, perform the following steps: 1. run `make` to build the extension (For Firefox, `build//linux-entra-sso-*.xpi` is generated) 2. Firefox only: Permit unsigned extensions in Firefox by setting `xpinstall.signatures.required` to `false` 3. Chrome only: In extension menu, enable `Developer mode`. 4. Install the extension in the Browser from the local `linux-entra-sso-*.xpi` file (Firefox). On Chrome, use `load unpacked` and point to `build/chrome` ## Usage After installing the extension, you might need to manually grant the following permission: - Access your data for `https://login.microsoftonline.com`. **No configuration is required.** The SSO is automatically enabled. If you want to disable the SSO for this session, click on the tray icon and select the guest account. In case you are already logged in, you might need to clear all cookies on `login.microsoftonline.com`. ### Single Page Applications For single-page applications (SPAs, like the Teams PWA) that perform automated re-logins in the background, ensure the extension has the necessary permissions to interact with the SPA's domain. Otherwise, a manual re-login after approximately 24 hours (depending on the tenant's configuration) may be required. To grant the necessary permissions, follow these steps: 1. Open the SPA URL in your web browser 2. Click on the extension's tray icon 3. Click on "Background SSO (enable)" 4. A dot should appear next to the domain indicating that permission has been granted Once configured, no further authentication requests will be needed. To revoke permissions, return to the extension's settings and select the domain again. For details, also see [PRIVACY.md](PRIVACY.md). ### Technical Background When enabled, the extension acquires a [PRT SSO Cookie](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oapxbc/105e4d17-defd-4637-a520-173db2393a4b) from the locally running `microsoft-identity-broker` service and inject that into the OAuth2 login flow on Microsoft Entra ID (`login.microsoftonline.com`). ## Known Limitations ### Snap version of Firefox on Ubuntu Running the extension in a Snap Firefox on Ubuntu 22.04 or later is supported but requires the `xdg-desktop-portal` host package and at least Firefox 104. After installing the extension (both native and web extension part), restart the browser. When Firefox starts, a message should appear to allow Firefox to use the `WebExtension` backend. Once granted, the application should behave as on a native install. An alternative is to use the `firefox-esr` Debian package. ### Flatpak Browsers installed as Flatpak are not officially supported, as they run in a sandbox that isolates them from the native messaging host. They may work when the [`xdg-native-messaging-proxy`](https://github.com/flatpak/xdg-native-messaging-proxy) is used to bridge into the sandbox, but this relies on several conditions that are beyond the control of `linux-entra-sso`. ## Troubleshooting In case the extension is not working, check the following: - run host component in interactive mode: `python3 ./linux-entra-sso.py --interactive acquirePrtSsoCookie` - check if SSO is working in the Edge browser # Code Integrity Since version `v0.4`, git release tags are signed with one of the following maintainer GPG keys: - `AF73F6EF5A53CFE304569F50E648A311F67A50FC` (Felix Moessbauer) - `004C647D7572CF7D72BDB4FB699D850A9F417BD8` (Jan Kiszka) Since version `v1.8.0`, the following keys are used: - `3785ED68D0F83B7BD7D23D7FE1136CEB2754A0BD` (Felix Moessbauer) - `004C647D7572CF7D72BDB4FB699D850A9F417BD8` (Jan Kiszka) ## License This project is licensed according to the terms of the Mozilla Public License, v. 2.0. A copy of the license is provided in `LICENSES/MPL-2.0.txt`. linux-entra-sso-1.11.0/biome.jsonc000066400000000000000000000010531525176353400170050ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: Copyright 2026 Siemens AG { "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", "files": { "includes": ["**", "!**/updates.json"] }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 4 }, "linter": { "enabled": false }, "assist": { "enabled": false }, "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true, "root": "." } } linux-entra-sso-1.11.0/docs/000077500000000000000000000000001525176353400156055ustar00rootroot00000000000000linux-entra-sso-1.11.0/docs/global_install.md000066400000000000000000000065741525176353400211310ustar00rootroot00000000000000 # System-Wide Install via Policy We support both system-wide installation and managed configuration. ## Host Components Linux distributions can include the host components by packaging the output of `make install` (using `DESTDIR` is supported). This makes the host parts available to all users, but requires the use of signed extension versions. The native messaging directories differ across Linux distributions. The variables `(firefox|chrome|chromium)_nm_dir` and `chrome_ext_dir` must be configured appropriately. The Python interpreter (shebang) is determined at install time to avoid runtime dependencies on venvs. This can be adjusted by setting `python3_bin`. The default values are suitable for a Debian system. For more information, refer to the `Makefile`. ## Webextension On Chrome, the `make install` target takes care of registering the extension to be auto-installed when starting the browser. On other browsers, the installation of the extension is controlled via a policy. The paths of the policy files may vary across browsers and distributions. On Debian, the following paths are known to work. ### Firefox Example: `/etc/firefox/policies/policies.json` ```json { "policies": { "ExtensionSettings": { "linux-entra-sso@example.com": { "installation_mode": "force_installed", "install_url": "file:///path/to/extension.xpi" } } } } ``` ### Chromium Example: `/etc/chromium/policies/managed/policies.json` ```json { "ExtensionSettings": { "jlnfnnolkbjieggibinobhkjdfbpcohn": { "runtime_allowed_hosts": ["https://login.microsoftonline.com"], "installation_mode": "force_installed", "update_url": "file:///path/to/chrome-update.xml" } } } ``` Chrome Update (`chrome-update.xml`) file: ```xml ``` ## Configuration We implement the `storage.managed` webextension API to allow injection of configuration. By that, a system administrator can configure settings of the extension via the policy files. > [!NOTE] > Some settings cannot be automatically enabled (e.g., granting permissions), as they require > user interaction. In this case, the extension detects a configuration update and notifies > the user via the tray icon. The user can then apply the changes by clicking a link in the > tray menu. ### Managed settings The settings are added to the policy file under `3rdparty.extensions.`. Example: ```json { "3rdparty": { "extensions": { "linux-entra-sso@example.com": { "wellKnownApps": { "example.com": true, } } } } } ``` #### `wellKnownApps` To allow background SSO, the extension needs the `host_permissions` for both the application domain, as well as for the login provider. Hereby, the app domain can be anything, but is usually known to the company managing the devices. Value: Dictionary of key-value pairs (string, bool), where the key is the domain and the value denotes if SSO is enabled. The domain must precisely match. Wildcards are not (yet) supported. ```json { "wellKnownApps": { "example.com": true, "another.example.com": false } } ``` linux-entra-sso-1.11.0/icons/000077500000000000000000000000001525176353400157705ustar00rootroot00000000000000linux-entra-sso-1.11.0/icons/linux-entra-sso.svg000066400000000000000000000047511525176353400215700ustar00rootroot00000000000000 linux-entra-sso-1.11.0/icons/linux-entra-sso_128.png000066400000000000000000000113041525176353400221370ustar00rootroot00000000000000‰PNG  IHDR€€Ã>aË pHYs¾–ûètEXtSoftwarewww.inkscape.org›î<QIDATxœí{tUÕÇ?¿s@^’DˆÒX-Bx(VlËèHµÕ™iY ¸–¶ÎjW[ˆé¬Y«3°l§1bíX;uTK‹UÊSðÔ EÀÀyÂ#!!{~óGÈ’äžssß÷†{>kݵtï³÷þ‘ý={ﳿ-ªŠCêb$Ú‡ÄâN´±Bî]îâôéþ¸ÍkP3èÑ!  à>‰GèœI¬¥‰E®„.@¾¶$›fß­†H‰šr#¢ÀÀÓeb¥ƒODùØTý\oëºGÄÜè$¡Û @n¯c˜Ü¯H è D·;;$ÂMSþÀ-§Þײ23Šy'ÝJmoºù-Aþtt| e¿ª>‡[_ÐUžŠK™q¤[@fT 4Z2…€ž 2£A„çLñü§®žs,A6D¤€L}®·!-?Tx è•h{.Ñ(èb³§÷çúúœ†D)I)¹w¹‹s'牲ÈM´=6|®"ótÍÜ?'ÚHH:È”Êáââw(ãCNlHvFNš–Ž¤e iiÐ+q¹ÐÖVhmE/6@C=œ;‡ž­Å<}Z[C·~gªç1]7ç\ȉ“€¤€L{úA~ ¤&=cP!ôë‘ÓÜaNm¨‰ž©E~Žyø3´î|(©ÿOU¿©ëæï ¯ðÄ‘’r·áÉý¥Â¼ ¸\W …×bôëHÔmÒÓ§0TaV¦/˜$Tùg]7oÙå2}q:Íi™¸% OkµçtYYsÔ “„ @¦/Níù*È]>l¸p -ÂQ =zÄÁ:ІzÌO?Á<¸Ì.§LA_Uä*`‡õüDpØ)°ÓT>ÂtmÖ ‰®õ]“PÈŒŠ,ñ«»ì£Æ q²®#zþ掿až8³"@vˆ°Êô±R×Ïý0V]N %ÏdˆGW>—‘‰ë¦ Hn~œ, Œyp¾BkKl R¶«A%uͯèû7ƪ˜„@JÊÝâÉ}˜è9ãša¸Æ|%ü]ŒÐúz|ïoDÏÕÆ£¸Ó ‹hIÿ/ÝøÐÅhgžå`Ã[I Êטq¸nžt• ¸KïÀ48Ååü\<>•iÏ̉îˆ7î-€L{úaA–Ø>àrã7cÀÀ8Z.ŠoÛ_1ìg¡Ôçz0ZƸ @¦,)Ãü»ù|—×-“1ú]7›"FÁܹ_Õ®¶ÿw»‘ÞÙÙ9Õ<p{Áe€¯š[Ж&8w=w=w¶-<4N«Ê]7wE¤æÇMro¹WÎæleù€aàš0£ÿÕq±'Ú˜'Ž"½Ò‘Ì,ZiÓ‡YsŽÆÚì¼Ã&m‘»tãÜà›KÄE®i•¯(Üggôɦ×Ô;áÒbCGZ¦yë´¡Ë•ç5Zë¹K·Î i‚"æßXr{å´Ž4H .Oò}ê% žƒè™×†¿m¦åóê@N7²[^áÛªýVÇü/o˜ò#E-GEi#FÐ37'Ö&tÒzà4™†=»¸°u+ØÔ¯ ÷S*¼ÁfÓ.@¦?](*û¿öÝðzé÷ÍYqZÔ¹Rh¬>ÄÙwßmÛ×`©0M×ÎÛL~1m SQñ¯|€¬Šé‘–¨í}ÝOá`<§Ö¯G­W' ßKÉs7èÆ95]å3ȽË]"Üogx=䌼Ã9—Þ‚«‘‰·rbÓÛv½ÁÃÓR‰ÝØë2bל=UJÛá ?²¯N]ŸÙp°'çÚ!˜õõœúp›e¼Â½2½²R×Ì{'P>1€¡æÝj3#–{Ýp<®èïâI5Œ-æâ±ãÔ¶^•_JyùØ@3…1€ŠÌ° OËË!3/;VŦ©·±ké Z›,âu4›sï^´Ë!&éKnl×wHgÎ'jx2{Q0~,ÞÚl/ð} jŸ2õWàþ'ý:p6;5ÇÌžIf~²nõ*Û_þõ§¬:«aÞ¦«}×*.â@¦VŒ\ E\3AŽë]n7ÙýsÃéÿ£‹0ì֛رb­e¬aº¢+)y¦¿áÖÅ"Æ·°™éëLV~_¼nçÛ/\5dû²{sáŒÿùEï–YºêQ¿Ãa @¦WÞ'žUèJº>ù¹NÿC†ŒÉÎu[¬¢¼´Êt`yçˆÐv€#¯Bàáp ì{U.§ù…Åײë­0}þ_}r‘@JžÉoîrU½=Ø4†ËEfß,zçe“•Û‡Ã9-@ ñ¤yÉ+èωƒþ{îA:¯%ùê/zIº÷u”’®žõöôR8j(#“M g?®X ÈeÊ’Bèèþ¦KHy¹!™¹¯tUù=zõ¤øÖbFÞ<·×™æM‡\ý‘"óK@h06ç<©pw g®-ÊWïO¯ôdñáºäå÷Æp–ã ¾¬¼<( dÚ’‰‚üÄ.ÞpLœùUFÝ4"Ls¢Žá";·7§OøO mV;`+™ù\šˆù«i`Ñ.€o˜ÓÅÂkrœ­ÜÝ€O÷ðc¸wtê(ËÝš.—A~NzHŽ/.çÐîYY¹8¼Ä äÖo~‹Ñ¥AoH v}ò¹]ÔAýËßuìÔhºU 㧇¼ýçOå×^ ;}¢:ê¾2µû öÌvWY_e `yX´“¬ßqC$¢æ¿»îut«nï÷÷bšÖŸð¦²Î*¼óg ¥‹‘Æ‹Í »Ñß°†Ðm¾>ŸÉêõ»í¢Ïs¡Ùò^ƒŽ9k嘨µÕ¤î|}³ƒöâÞ±nòGìŒKè6Ÿ¾o½³c'¬?ÿDyÍ´q"ÕQ¦o?b][GŽ%?Ço-!(\Ýt'°Ën±‹¹¥ÕÇK+¬O ˜È vqÐÃ]E³õ7ä'{Ž1î†ð|øjFo¾ùf¼^Ûõ븰eË|>ÿ;\Þnм´bÕG¬gÿ€÷tÝÜ÷ì";@ßxäŒ1­r/0¼óƒ[¶â»÷ÈP+^{í5 õ|C!++‹º:¿iònÁ‘ãçùí«[mãÕ””ÞïÝØhõàΪã>Ö-¯Å¹biiõñ“Ek¹Ødã/HØ¢ëç®”‡ŸLt•Õƒª°ôχc§CŒ¨xa »öž°‹ö©0¿«<ü{çZï_Ë2ÿ´v7'O‡ìŒÒ!,ýóǼòúG¶ñ‚.ÑÕó¶w•Ÿt뜱q(ÐÔÔÊS¿±O8ĉ՛öò‹ÀõpÀTïOƒÉËr|nžÅ€å®‚ ›÷óæ[~‹JqbÙ›;){j=¦½c5y Ø{ - «çxÞ.Ñ,y›Ý{O“¿C”ðùL*^xŸEϾ¨òQÕèúy–gÄ­°ýB7æ…ØŒ/¶ðXùì?—;sRž“§ëyøÇâ÷+þð9_éºù¡äm+]ýx­Š“_¿ôëÞÝÇü‡ÆsËØÁaoIu[xkË~^Yù{téæQ}Ƽ¥æÑp¯éRºæ‰ 2£b†´ïa1EÜÎêZ¾ÿ³7¹¶0‡™SG2å–a䆹z GŽaÑ¢EìÙ³‡±cÇòÄOÝýPž<]χaËöC¼ý׃ö³zþ´ªðcsíüE‘”´Ÿ@)}öjqµ®¶½ôÉ‚!}¹nHZ³‹U¿xÌò™êêê×öìÙCii)ÇŽ}Ñ/Ž1‚ 6pÕU¡ß8f·0cιíÞŸM­46¶ÐÐØÂ©Úzªœå³Ãg9~*¬5ˆÃª2;Ð"O°„ä(R¾¶$[šÌ? ]»Šé@ÍnôïÏXF…*€={ö0yòdŽ?îWTTÄÆCdø7àšÒòŠ1*¢/šÍÞã >BÚª¡oÜ_QQ‘=zÔ¯,UÕÌÌL»tOÄãï“È_ÒíØ¿xñ"wÞy''NØnu"/ÏÚOAn®ýEUUUÜsÏ=Ûw¥‘tØ´i´/++cÖ¬Y–q?þ8=ômÚÍ›7SUÿÍ,"2LD^‘ý"²FD¢¿½:L’N-öŽ(++ãÉ'Ÿ´7 ƒçŸ> å D¤7ð60 L6ŠHa\ ±!éPRRBÿþýý»ªüv‰ ¸¸˜ë¯·t€K¾Žÿý‰ÀwâmˆI'€ŒŒ V­ZÅèÑ£¶~½²²2¨Êo§] .$33aÒ¤I¬\¹2þ ýÕ8<®$å½í£GfûöíÔÕÕ‘žžŽÆ³†aP^^΂ hjj"==¼sQ ©wG$¥ÚÉÌÌŒ8·ÛÛÔÿÌ„’t]€C|qâ8Hq¤8ŽRG)Ž#€Ç@Šã Åqâ8Hq¤8ŽRG)Ž#€‘yQDNŠH•ˆ„uœXDî‘D¤FDVŠÈàèZjM·\(ïÛ×úœ8[À[/ýwð¹FU›ˆ|ø5_l™ Œ‘QªØ=H„tË`öìÙx<¯GíÓ§3gÎŒ«"RÄ•9?‘ 2ΕßÎ0`RDA·À¨Q£X¹r%#GŽDD¸é¦›X³f ùùùñ6Åæ–f ¨üvbïF=OÅ›pOÑö}b÷o»ôÛo~0¤;d*?¿nÙ$ ªjÿ:½:Ä&|0öo~=0KUcîÃÞ@„¨ê§ÀDàh”²¼Ü¥ªqqÊì ¨ê^ ”À-A0Ô·«ê¦ˆ GQ" -A\ßüvD‘Z‚¸¿ùí8ˆ2—Z‚‚A=pG¼ßüvÄ€DÐÊG1#$¼òÁ@L¹LÕ‘•ŽbÎ%ŒþØGÛâÑ„d¨|H‚ÕÀŠŠ ²²²jCsslw©ê!àÛ1-$L.€E‹"òvî!Nâ8Hq¤8ŽRœx ›°¹ƒ0ÉiH´±&.΢’§ Hq¤8ŽRœÿ!øç1s*?ÔIEND®B`‚linux-entra-sso-1.11.0/icons/linux-entra-sso_128.png.license000066400000000000000000000001241525176353400235560ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/icons/linux-entra-sso_48.png000066400000000000000000000035301525176353400220620ustar00rootroot00000000000000‰PNG  IHDR00Wù‡ pHYsßߨ tEXtSoftwarewww.inkscape.org›î<åIDAThíšL”÷Ç_ŸçŽãç°'ˆ‡uVtE·ÑdÑNª"ˆ.Ù–të²uË–ŒY’Ìt[·5 s3[ç\j‡Øº4™M4±%Ý®vC˜6 dNJ;ai ‚øƒbùqˆÀq÷<Ÿýq pw tI_É“»çûãóy¿ïû|ŸïóãDUùÆ>AdkÙzÃÐíŠlRGcw-¢ú¶…íMœ•úZÉð|äËéȾ}瓾+"?>Z/íP‘R¢£^ÐS;Ý%ž¬#RP¶V,ŽYc…v;Æ’¥È¢Åhlb¨×wúÐÎÔ341D«Šþ@+öœ½ï$¯ì ޱâLÂX³cY:¶à´û¬ËMX7¥¦ŠîGeȧnTÚ0´EÝÿ õP Ë€äþ¶ Çv;¶¬/`¬È 9VG;V}í䙌K ÜRë¨V¿3£¦P ÈÖ²õbðàØ8l9yHâ! Ÿˆº1«+Ñþ>°Û‘˜ð™ÁL™eÖˆ<«ç~8±Ùv0^4ö= G4öÜHBbDâÇF=œÀš&zÛ…^oż~†½Ö­*ºU+ö\È€mÛ‘ýªú,@LN.ö´OÌM|¨×Ëð»ik¿ÄëjZ[ôŸÅ-ÛÍj@6I(mc|6m¾g¢ƒáni¡¯ö<¨Ò¬ÞÏjÝÞ±cmö…Ì¡ßBIXú¹,¢aÌØy ñáUÄÚáÖ[5€f ŽàgúY ˆò%€ø”d–¤&Ý;¥3¸.ƒ‘övzš[QxZ¶¿øýûîË0ùòŸãðøv º X¼üã$Þç_"™¹ë©k»92b7,ó{ÀÏ!ÈmeÙ¢gTx€ìÇ·’¶zÅ}’œúSç¸ñ߀úh÷ -)±îÉñ1Ó@<@ÚÊe¬ÌÊ 9-™Ä¤EÈ «³V ¤S“²¸4f@ò§ RÄGEG‘÷õM¬Ì\±@RƒóÉ•)ã;†®e¢yNa‰a|ãû,(u$ÎLbt,6» Óg‚ZÉ0:‰%÷¥ebã›_ÜüiÖ=œ¶:§EUG×À0Lœ…lævÀ.ùŸ!1zöièï÷`šÖèžõ>Œ0 K´T'Ë—&,”¾Yi{¿g|ÇÒk•dDIrÆñ±è©«ÿz’?îúΜ’g<òyVÔÍ)Fã¥Ñk9áºþc ™öx=#A«hÃB-kJy8ˆeÎi!¼ÓîŠ?ú7«¤Ä‚Q–A›(´Ýta³|ÄÆDÝÕ9Æ6žøÐ¡C8ŽŸ8q‚ÚÚZl$ÌÁÀÑ×ëô˜ö?Êý#`§1¬ßz½>*ª›ùj~æ´ ‰9qCCµµµ 8[×Ê«o\@”W¬3»šu€Ví¾\8zü=½óòÀ`^8ñ¿z¾*pö¼jù¢ž™Xo¾¨aý°º{Ýìýõt/° ×ÇÑxú7§òŒ ¨êWôÜÎî‰íÆ ü£¸Fáï]éâÉ⓼vº‘þ/³ÑÙÙIcc㼿Ö~›cå <þÔq^>Yåÿéoª›´rÏ”$w]Ì院ßKÞaSDžsõ ÙþðR5Ï¿|§»ir¿1šššÈÍÍ¥««‹ÒÒRŠŠŠ¦VôËSÁƒ¨ÒsÛM—ËÍ~ÏäÚS:"Oé¹Ý·‚ur? •{ʶ²Q9>ŸEWÏà´â·lÙBWWÅÅÅAM º‡ù÷»7‚˜Ê0ÂY…ßiEQõL ƒÞÐhEÑà1)8² ÓÚ.C];€‚™ÄƒÿZeZ¾¡^Aª‚ªP,D?°T:0¸‚e¯Ô3;ûfµH:Û<‰ÿÙ€ hgg§¦¤¤( N§Sm6›šœœ¬€Šˆ–——«ªjaa¡Žö½8Cüh`u(Z&oFp[3ãr¹èîîfñâÅTUUa·û²´´”œœT•æææpB.‹È×ÂÕÑãõ5kÖÐØØˆÓé$5uü¾!>>žÊÊJêëëÉÎÎ'dƤωøý@ffðÕÚáp°aÆHÆMD‡Ð‡‰ ,4ÆN£ÏP‘ý"rXDb&•gŠÈë"²#¤ÜáŽ}ûöÑÐÐÀÆCj/"qø øOk)€xsÖ`‘¬Äá0ÝJ ü83Èæ¹g+ñ| ªg‚Tõyª:ã»± :‰Uõw›ˆ;Ôó2悪‘Aà `¯ª6„Ó?léééaµw»g¿5UÕ2 ,\-ÞÞÞHòÜ3B5ð£oDæ@Çû%â?{|Xøˆí† ;aÝÊIEND®B`‚linux-entra-sso-1.11.0/icons/linux-entra-sso_48.png.license000066400000000000000000000001241525176353400234770ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/icons/profile-outline.svg000066400000000000000000000015701525176353400216310ustar00rootroot00000000000000 linux-entra-sso-1.11.0/icons/profile-outline_48.png000066400000000000000000000012511525176353400221250ustar00rootroot00000000000000‰PNG  IHDRàw=ø pHYsÃÃÇo¨dtEXtSoftwarewww.inkscape.org›î<6IDATH‰µ–?hqÇ?¿ß]2¥˜…‚àä\¡¨›h…:ˆ‹“%»ƒJE,¸8Ø©ƒ:(4í%¸•*çØ©ƒPœũЂZÝ”#¶„Üï¹\ ¦I.´ä»Üã¾ï}¿ï÷¸wü”ˆ0J葪ö0Ižçi9  ”ú,,---'Õª¤¹®û˜éC?ö}ÿæ ú#ò<ïR,¾%"ÓÆ˜qcÌ8pØfÇ™¤1pD"âÆá|­V«vP Žãä•RsJ)XÛÕ €³Q=íÁ=‹Ÿg $äcƒÝD&“ÙŒÃ{1ØÐZïï&F.·öbð •Jé&Òéô±8ü²ƒu)uƘRgή Dä %×uÏ·ß;Žs(QœÓÃ,Úàvl´®”R"r°€‡¾ïϪOüU„ax'›Í¶”R·€É¸¡&p? ûIõ‰'h£\.´m{@)õÆ÷ý_ÃÔ m°[ôQ¡P°²Ùì¥ÔeàpØÓ àðX Ãp5‚¨—NÏxž7!"ËÀñ!Ý0ÆëõúÛDƒJ¥rBký>+Ƙµt:ý³Õjý°m;ßl6i­§€ëÀQàeY§«ÕêÇN½#²,랈ŒÏÃ0,AÐìÑñ_`xU(æs¹Ü p5Š¢9àZgâŽE‘ q8ÛGü?AÐÔZ·wa²›ïµÉÀKß÷¿'‰·±¸¸øx!"ﺹ‘¦#¿Uüd+ÖÅäVÕ6IEND®B`‚linux-entra-sso-1.11.0/icons/profile-outline_48.png.license000066400000000000000000000001241525176353400235440ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2021 YANDEX LLC SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/linux-entra-sso.py000077500000000000000000000304321525176353400203040ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MPL-2.0 # SPDX-FileCopyrightText: Copyright 2024 Siemens AG # pylint: disable=missing-docstring,invalid-name # Renable invalid-name check, it should only cover the module name # pylint: enable=invalid-name import argparse import json import struct import sys import uuid from enum import Enum from threading import RLock, Thread from xml.etree import ElementTree as ET from gi.repository import GLib from pydbus import SessionBus from pydbus.proxy import CompositeInterface # version is replaced on installation LINUX_ENTRA_SSO_VERSION = "0.0.0-dev" # the ssoUrl is a mandatory parameter when requesting a PRT SSO # Cookie, but the correct value is not checked as of 30.05.2024 # by the authorization backend. By that, a static (fallback) # value can be used, if no real value is provided. SSO_URL_DEFAULT = "https://login.microsoftonline.com/" EDGE_BROWSER_CLIENT_ID = "d7b530a4-7680-4c23-a8bf-c52c121d2e87" # dbus start service reply codes START_REPLY_SUCCESS = 1 START_REPLY_ALREADY_RUNNING = 2 # stripped down version of the broker dbus interface, # as brokers > 2.0.1 do not implement introspection BROKER_DBUS_SPEC = r""" """ class AuthorizationType(Enum): CACHED_REFRESH_TOKEN = (1,) PRT_SSO_COOKIE = (8,) class NativeMessaging: @staticmethod def get_message(): """ Read a message from stdin and decode it. """ raw_length = sys.stdin.buffer.read(4) if not raw_length: sys.exit(0) message_length = struct.unpack("@I", raw_length)[0] message = sys.stdin.buffer.read(message_length).decode("utf-8") return json.loads(message) @staticmethod def encode_message(message_content): """ Encode a message for transmission, given its content """ encoded_content = json.dumps(message_content, separators=(",", ":")).encode( "utf-8" ) encoded_length = struct.pack("@I", len(encoded_content)) return {"length": encoded_length, "content": encoded_content} @staticmethod def send_message(encoded_message): """ Send an encoded message to stdout """ sys.stdout.buffer.write(encoded_message["length"]) sys.stdout.buffer.write(encoded_message["content"]) sys.stdout.buffer.flush() class SsoMib: BROKER_NAME = "com.microsoft.identity.broker1" BROKER_PATH = "/com/microsoft/identity/broker1" GRAPH_SCOPES = ["https://graph.microsoft.com/.default"] def __init__(self, daemon=False): self._bus = SessionBus() self.broker = None self.session_id = uuid.uuid4() self._state_changed_cb = None if daemon: self._introspect_broker() self._monitor_bus() def check_broker_online(self): return bool(self._bus.get(".DBus").NameHasOwner(self.BROKER_NAME)) def _introspect_broker(self): introspection = ET.fromstring(BROKER_DBUS_SPEC) self.broker = CompositeInterface(introspection)( self._bus, self.BROKER_NAME, self.BROKER_PATH ) def _monitor_bus(self): self._bus.subscribe( sender="org.freedesktop.DBus", object="/org/freedesktop/DBus", signal="NameOwnerChanged", arg0=self.BROKER_NAME, signal_fired=self._broker_state_changed, ) def _broker_state_changed( self, sender, object, iface, signal, params ): # pylint: disable=redefined-builtin,too-many-arguments _ = (sender, object, iface, signal) # params = (name, old_owner, new_owner) new_owner = params[2] # the broker introspection is static, so the proxy object never # needs to be (re)created or dropped here; just report the state. self._report_state_change(bool(new_owner)) def _report_state_change(self, online): if self._state_changed_cb: self._state_changed_cb(online) def on_broker_state_changed(self, callback): """ Register a callback to be called when the broker state changes. The callback should accept a single boolean argument, indicating if the broker is online or not. """ self._state_changed_cb = callback @staticmethod def _get_auth_parameters(account, scopes, sso_url=None): params = { "account": account, "additionalQueryParametersForAuthorization": {}, "authority": "https://login.microsoftonline.com/common", "authorizationType": ( AuthorizationType.PRT_SSO_COOKIE.value[0] if sso_url else AuthorizationType.CACHED_REFRESH_TOKEN.value[0] ), "clientId": EDGE_BROWSER_CLIENT_ID, "redirectUri": "https://login.microsoftonline.com" "/common/oauth2/nativeclient", "requestedScopes": scopes, "username": account["username"], "uxContextHandle": -1, } if sso_url: params["ssoUrl"] = sso_url return params def get_accounts(self): self._introspect_broker() context = { "clientId": EDGE_BROWSER_CLIENT_ID, "redirectUri": str(self.session_id), } # pylint: disable=maybe-no-member resp = self.broker.getAccounts("0.0", str(self.session_id), json.dumps(context)) return json.loads(resp) def acquire_prt_sso_cookie( self, account, sso_url, scopes=GRAPH_SCOPES ): # pylint: disable=dangerous-default-value self._introspect_broker() request = { "account": account, "authParameters": SsoMib._get_auth_parameters(account, scopes, sso_url), "mamEnrollment": False, "ssoUrl": sso_url, } # pylint: disable=maybe-no-member token = json.loads( self.broker.acquirePrtSsoCookie( "0.0", str(self.session_id), json.dumps(request) ) ) return token def acquire_token_silently( self, account, scopes=GRAPH_SCOPES ): # pylint: disable=dangerous-default-value self._introspect_broker() request = { "authParameters": SsoMib._get_auth_parameters(account, scopes), } # pylint: disable=maybe-no-member token = json.loads( self.broker.acquireTokenSilently( "0.0", str(self.session_id), json.dumps(request) ) ) return token def get_broker_version(self): self._introspect_broker() params = json.dumps({"msalCppVersion": LINUX_ENTRA_SSO_VERSION}) # pylint: disable=maybe-no-member resp = json.loads( self.broker.getLinuxBrokerVersion("0.0", str(self.session_id), params) ) resp["native"] = LINUX_ENTRA_SSO_VERSION return resp def run_as_native_messaging(): iomutex = RLock() def respond(command, message): NativeMessaging.send_message( NativeMessaging.encode_message({"command": command, "message": message}) ) def notify_state_change(online): with iomutex: respond("brokerStateChanged", "online" if online else "offline") def handle_command(cmd, received_message): if cmd == "acquirePrtSsoCookie": account = received_message["account"] sso_url = received_message["ssoUrl"] or SSO_URL_DEFAULT token = ssomib.acquire_prt_sso_cookie(account, sso_url) respond(cmd, token) elif cmd == "acquireTokenSilently": account = received_message["account"] scopes = received_message.get("scopes") or ssomib.GRAPH_SCOPES token = ssomib.acquire_token_silently(account, scopes) respond(cmd, token) elif cmd == "getAccounts": respond(cmd, ssomib.get_accounts()) elif cmd == "getVersion": respond(cmd, ssomib.get_broker_version()) def run_dbus_monitor(): # inform other side about the current broker state notify_state_change(ssomib.check_broker_online()) loop = GLib.MainLoop() loop.run() print("Running as native messaging instance.", file=sys.stderr) print("For interactive mode, start with --interactive", file=sys.stderr) ssomib = SsoMib(daemon=True) ssomib.on_broker_state_changed(notify_state_change) # daemon=True is critical: without it, the GLib main loop thread # keeps the process alive as an orphan after the browser closes stdin. monitor = Thread(target=run_dbus_monitor, daemon=True) monitor.start() while True: received_message = NativeMessaging.get_message() with iomutex: cmd = received_message["command"] try: handle_command(cmd, received_message) except Exception as exp: # pylint: disable=broad-except err = {"error": f"Failure during request processing: {str(exp)}"} respond(cmd, err) def run_interactive(): def _get_account(accounts, idx): try: return accounts["accounts"][idx] except IndexError: json.dump( {"error": f"invalid account index {idx}"}, indent=2, fp=sys.stdout, ) print() sys.exit(1) parser = argparse.ArgumentParser() parser.add_argument( "-i", "--interactive", action="store_true", help="run in interactive mode", ) parser.add_argument( "-a", "--account", type=int, default=0, help="account index to use for operations", ) parser.add_argument( "-s", "--ssoUrl", default=SSO_URL_DEFAULT, help="ssoUrl part of SSO PRT cookie request", ) parser.add_argument( "command", choices=[ "getAccounts", "getVersion", "acquirePrtSsoCookie", "acquireTokenSilently", "monitor", ], ) args = parser.parse_args() monitor_mode = args.command == "monitor" ssomib = SsoMib(daemon=monitor_mode) if monitor_mode: print("Monitoring D-Bus for broker availability.") ssomib.on_broker_state_changed( lambda online: print( f"{ssomib.BROKER_NAME} is now " f"{'online' if online else 'offline'}." ) ) GLib.MainLoop().run() return accounts = ssomib.get_accounts() if len(accounts["accounts"]) == 0: print("warning: no accounts registered.", file=sys.stderr) if args.command == "getAccounts": json.dump(accounts, indent=2, fp=sys.stdout) elif args.command == "getVersion": json.dump(ssomib.get_broker_version(), indent=2, fp=sys.stdout) elif args.command == "acquirePrtSsoCookie": account = _get_account(accounts, args.account) cookie = ssomib.acquire_prt_sso_cookie(account, args.ssoUrl) json.dump(cookie, indent=2, fp=sys.stdout) elif args.command == "acquireTokenSilently": account = _get_account(accounts, args.account) token = ssomib.acquire_token_silently(account) json.dump(token, indent=2, fp=sys.stdout) # add newline print() if __name__ == "__main__": if "--interactive" in sys.argv or "-i" in sys.argv: run_interactive() else: run_as_native_messaging() linux-entra-sso-1.11.0/platform/000077500000000000000000000000001525176353400165015ustar00rootroot00000000000000linux-entra-sso-1.11.0/platform/chrome/000077500000000000000000000000001525176353400177565ustar00rootroot00000000000000linux-entra-sso-1.11.0/platform/chrome/extension.json000066400000000000000000000001211525176353400226570ustar00rootroot00000000000000{ "external_update_url": "https://clients2.google.com/service/update2/crx" } linux-entra-sso-1.11.0/platform/chrome/extension.json.license000066400000000000000000000001241525176353400243030ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/platform/chrome/get-ext-id.py000077500000000000000000000007611525176353400223060ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright 2024 Siemens AG # SPDX-License-Identifier: MPL-2.0 # # Compute the extension ID from the path of the extension # (for unpacked extensions). import hashlib import sys import os if len(sys.argv) != 2: print("Usage: python get-ext-id.py ") PATH = os.path.realpath(sys.argv[1]) m = hashlib.sha256() m.update(bytes(PATH.encode("utf-8"))) EXTID = "".join([chr(int(i, base=16) + ord("a")) for i in m.hexdigest()][:32]) print(EXTID) linux-entra-sso-1.11.0/platform/chrome/js/000077500000000000000000000000001525176353400203725ustar00rootroot00000000000000linux-entra-sso-1.11.0/platform/chrome/js/platform-chrome.js000066400000000000000000000100301525176353400240210ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { Platform } from "./platform.js"; import { getLogger } from "./utils.js"; const log = getLogger("platform"); export class PlatformChrome extends Platform { browser = "Chrome"; #broker = null; #on_alarm_cb = null; static CHROME_PRT_SSO_REFRESH_INTERVAL_MIN = 30; static PRT_SSO_REFRESH_ALARM = "prt-sso-refresh"; constructor() { super(); /* * Register the alarm listener synchronously during worker startup. * Only top-level listeners can wake a suspended MV3 service worker, * so this is required for the periodic PRT refresh to fire reliably. */ this.#on_alarm_cb = this.#on_alarm.bind(this); chrome.alarms.onAlarm.addListener(this.#on_alarm_cb); } update_request_handlers(enabled, account, broker) { super.update_request_handlers(enabled, account, broker); this.#broker = broker; if (!enabled) { chrome.alarms.clear(PlatformChrome.PRT_SSO_REFRESH_ALARM); this.#clear_net_rules(); return; } this.#ensure_refresh_alarm(PlatformChrome.PRT_SSO_REFRESH_ALARM); this.#update_net_rules(broker); } /* * Called when the refresh alarm fires (possibly waking the worker). * If the broker/account are not ready yet, the regular startup flow will * refresh the rules, so we can safely skip here. */ #on_alarm(alarm) { if (alarm.name !== PlatformChrome.PRT_SSO_REFRESH_ALARM) return; if (!this.#broker || !this.account) return; this.#update_net_rules(this.#broker); } /* * Ensure the alarm is armed with the configured period. If an alarm with * a different period already exists, it is re-created. */ async #ensure_refresh_alarm(alarm_id) { const period = PlatformChrome.CHROME_PRT_SSO_REFRESH_INTERVAL_MIN; const alarm = await chrome.alarms.get(alarm_id); if (alarm && alarm.periodInMinutes === period) { return; } await chrome.alarms.create(alarm_id, { periodInMinutes: period, }); } async #clear_net_rules() { log.debug("clear network rules"); const oldRules = await chrome.declarativeNetRequest.getSessionRules(); const oldRuleIds = oldRules.map((rule) => rule.id); await chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: oldRuleIds, }); this.clear_error(); } async #update_net_rules(broker) { log.debug("update network rules"); let prt = undefined; try { prt = await broker.acquirePrtSsoCookie( this.account, Platform.SSO_URL, ); } catch (error) { this.report_error("Failed to acquire the SSO token: " + error); return; } const newRules = [ { id: 1, priority: 1, condition: { urlFilter: "|" + Platform.SSO_URL + "/", requestDomains: [URL.parse(Platform.SSO_URL).hostname], resourceTypes: ["main_frame", "sub_frame"], }, action: { type: "modifyHeaders", requestHeaders: [ { header: prt.cookieName, operation: "set", value: prt.cookieContent, }, ], }, }, ]; const oldRules = await chrome.declarativeNetRequest.getSessionRules(); const oldRuleIds = oldRules.map((rule) => rule.id); // Use the arrays to update the dynamic rules await chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: oldRuleIds, addRules: newRules, }); this.clear_error(); log.debug("network rules updated"); } } linux-entra-sso-1.11.0/platform/chrome/js/platform-factory.js000066400000000000000000000003351525176353400242220ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { PlatformChrome } from "./platform-chrome.js"; export function create_platform() { return new PlatformChrome(); } linux-entra-sso-1.11.0/platform/chrome/linux_entra_sso.json000066400000000000000000000005031525176353400240630ustar00rootroot00000000000000{ "name": "linux_entra_sso", "description": "Entra ID SSO via Microsoft Identity Broker", "path": "/usr/local/lib/linux-entra-sso/linux-entra-sso", "type": "stdio", "allowed_origins": [ "chrome-extension://{extension_id}/", "chrome-extension://jlnfnnolkbjieggibinobhkjdfbpcohn/" ] } linux-entra-sso-1.11.0/platform/chrome/linux_entra_sso.json.license000066400000000000000000000001241525176353400255030ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/platform/chrome/manifest.json000066400000000000000000000017241525176353400224630ustar00rootroot00000000000000{ "description": "Entra ID SSO via Microsoft Identity Broker", "manifest_version": 3, "name": "Linux Entra SSO", "version": "1.11.0", "icons": { "48": "icons/linux-entra-sso_48.png", "128": "icons/linux-entra-sso_128.png" }, "action": { "default_popup": "popup/menu.html", "default_icon": { "48": "icons/linux-entra-sso_48.png", "128": "icons/linux-entra-sso_128.png" }, "default_title": "Linux Entra SSO", "default_area": "navbar" }, "background": { "service_worker": "/src/background.js", "type": "module" }, "permissions": [ "alarms", "nativeMessaging", "declarativeNetRequest", "storage", "activeTab" ], "host_permissions": ["https://login.microsoftonline.com/*"], "optional_host_permissions": ["https://*/*"], "storage": { "managed_schema": "storage-schema.json" } } linux-entra-sso-1.11.0/platform/chrome/manifest.json.license000066400000000000000000000001241525176353400240750ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/platform/chrome/storage-schema.json000066400000000000000000000004341525176353400235540ustar00rootroot00000000000000{ "type": "object", "properties": { "wellKnownApps": { "description": "Apps that are allowed to perform background SSO.", "type": "object", "additionalProperties": { "type": "boolean" } } } } linux-entra-sso-1.11.0/platform/chrome/storage-schema.json.license000066400000000000000000000001241525176353400251710ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2025 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/platform/firefox/000077500000000000000000000000001525176353400201435ustar00rootroot00000000000000linux-entra-sso-1.11.0/platform/firefox/amo-metadata.json000066400000000000000000000014121525176353400233660ustar00rootroot00000000000000{ "summary": { "en-US": "Browser plugin for Linux to SSO on Microsoft Entra ID using a locally running microsoft identity broker (Intune)." }, "description": { "en-US": "This extension will only work on intune-enabled Linux devices. Please double check this by running the intune-portal application and check if your user is logged in (after clicking sign-in). The plugin also needs a host script that communicates with the microsoft-identity-broker via DBus. For details, see the plugin documentation." }, "website": { "en-US": "https://github.com/siemens/linux-entra-sso" }, "categories": ["privacy-security"], "version": { "license": "MPL-2.0" } } linux-entra-sso-1.11.0/platform/firefox/amo-metadata.json.license000066400000000000000000000001241525176353400250060ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/platform/firefox/js/000077500000000000000000000000001525176353400205575ustar00rootroot00000000000000linux-entra-sso-1.11.0/platform/firefox/js/platform-factory.js000066400000000000000000000003401525176353400244030ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { PlatformFirefox } from "./platform-firefox.js"; export function create_platform() { return new PlatformFirefox(); } linux-entra-sso-1.11.0/platform/firefox/js/platform-firefox.js000066400000000000000000000104151525176353400244020ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { Platform } from "./platform.js"; import { getLogger, Deferred } from "./utils.js"; import { StateMachine } from "./state-machine.js"; const log = getLogger("platform"); /* * Whether PRT injection can be performed. UNKNOWN means that the event page * was just woken and did not restore its state yet. */ const InjectionState = Object.freeze({ UNKNOWN: "unknown", ACTIVE: "active", INACTIVE: "inactive", }); const INJECTION_TRANSITIONS = Object.freeze({ [InjectionState.UNKNOWN]: [InjectionState.ACTIVE, InjectionState.INACTIVE], [InjectionState.ACTIVE]: [InjectionState.INACTIVE], [InjectionState.INACTIVE]: [InjectionState.ACTIVE], }); class InjectionStateMachine extends StateMachine { /* pending until the startup left the UNKNOWN state */ #known = new Deferred(); constructor() { super("injection-state", INJECTION_TRANSITIONS, InjectionState.UNKNOWN); } is_active() { return this.is_in(InjectionState.ACTIVE); } is_known() { return !this.is_in(InjectionState.UNKNOWN); } set_active(active) { this.transition( active ? InjectionState.ACTIVE : InjectionState.INACTIVE, ); this.#known?.resolve(); this.#known = null; } /* * Block the caller until the startup reported a state, but no longer * than the given time. */ async await_known(timeout_ms) { if (!this.#known) return; const timeout = new Promise((resolve) => setTimeout(resolve, timeout_ms), ); await Promise.race([this.#known.promise, timeout]); } } export class PlatformFirefox extends Platform { browser = "Firefox"; /* how long a blocked request waits for the startup to report a state */ static STATE_TIMEOUT_MS = 5 * 1000; #broker = null; #injection = new InjectionStateMachine(); constructor() { super(); /* * Register the handler synchronously during page evaluation, as only * such listeners can wake a suspended event page. */ chrome.webRequest.onBeforeSendHeaders.addListener( this.#onBeforeSendHeaders.bind(this), { urls: [Platform.SSO_URL + "/*"], types: ["main_frame", "sub_frame"], }, ["blocking", "requestHeaders"], ); } setIconDisabled() { chrome.action.setIcon({ path: "/icons/linux-entra-sso.svg", }); } update_request_handlers(enabled, account, broker) { super.update_request_handlers(enabled, account, broker); this.#broker = broker; this.#injection.set_active(Boolean(enabled && account && broker)); this.clear_error(); } async #onBeforeSendHeaders(e) { const headers = { requestHeaders: e.requestHeaders }; // filter out requests that are not part of the OAuth2.0 flow const url = URL.parse(e.url); if ( url?.protocol !== "https:" || url.origin !== URL.parse(Platform.SSO_URL).origin ) { return headers; } /* a woken event page has not restored its state yet */ await this.#injection.await_known(PlatformFirefox.STATE_TIMEOUT_MS); if (!this.#injection.is_known()) { this.report_error( "Timed out while restoring the SSO state. " + "Requests are sent without SSO, please reload the page.", ); return headers; } if (!this.#injection.is_active()) { log.warn("SSO not available, pass request unmodified"); return headers; } try { let prt = await this.#broker.acquirePrtSsoCookie( this.account, e.url, ); // ms-oapxbc OAuth2 protocol extension log.debug("inject PRT SSO into request headers"); e.requestHeaders.push({ name: prt.cookieName, value: prt.cookieContent, }); this.clear_error(); } catch (error) { this.report_error("Failed to acquire the SSO token: " + error); } return headers; } } linux-entra-sso-1.11.0/platform/firefox/linux_entra_sso.json000066400000000000000000000004101525176353400242450ustar00rootroot00000000000000{ "name": "linux_entra_sso", "description": "Entra ID SSO via Microsoft Identity Broker", "path": "/usr/local/lib/linux-entra-sso/linux-entra-sso", "type": "stdio", "allowed_extensions": ["linux-entra-sso@example.com", "@linux-entra-sso.tb"] } linux-entra-sso-1.11.0/platform/firefox/linux_entra_sso.json.license000066400000000000000000000001241525176353400256700ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/platform/firefox/manifest.json000066400000000000000000000021411525176353400226420ustar00rootroot00000000000000{ "description": "Entra ID SSO via Microsoft Identity Broker", "manifest_version": 3, "name": "Linux Entra SSO", "version": "1.11.0", "icons": { "48": "icons/linux-entra-sso.svg" }, "browser_specific_settings": { "gecko": { "id": "linux-entra-sso@example.com", "strict_min_version": "140.0", "data_collection_permissions": { "required": ["authenticationInfo"] } }, "gecko_android": { "strict_min_version": "142.0" } }, "action": { "default_popup": "popup/menu.html", "default_icon": "icons/linux-entra-sso.svg", "default_title": "Linux Entra SSO", "default_area": "navbar" }, "background": { "scripts": ["/src/background.js"], "type": "module" }, "permissions": [ "nativeMessaging", "webRequest", "webRequestBlocking", "storage", "activeTab" ], "host_permissions": ["https://login.microsoftonline.com/*"], "optional_host_permissions": ["https://*/*"] } linux-entra-sso-1.11.0/platform/firefox/manifest.json.license000066400000000000000000000001241525176353400242620ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/platform/thunderbird/000077500000000000000000000000001525176353400210135ustar00rootroot00000000000000linux-entra-sso-1.11.0/platform/thunderbird/js/000077500000000000000000000000001525176353400214275ustar00rootroot00000000000000linux-entra-sso-1.11.0/platform/thunderbird/js/platform-factory.js000066400000000000000000000003541525176353400252600ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { PlatformThunderbird } from "./platform-thunderbird.js"; export function create_platform() { return new PlatformThunderbird(); } linux-entra-sso-1.11.0/platform/thunderbird/js/platform-thunderbird.js000066400000000000000000000005071525176353400261230ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { PlatformFirefox } from "./platform-firefox.js"; export class PlatformThunderbird extends PlatformFirefox { browser = "Thunderbird"; transform_ui_title(title) { return title.split(/[@(]/)[0].trim(); } } linux-entra-sso-1.11.0/platform/thunderbird/manifest.json000066400000000000000000000024011525176353400235110ustar00rootroot00000000000000{ "description": "Entra ID SSO via Microsoft Identity Broker", "manifest_version": 3, "name": "Linux Entra SSO", "version": "1.11.0", "icons": { "48": "icons/linux-entra-sso.svg" }, "browser_specific_settings": { "gecko": { "id": "@linux-entra-sso.tb", "strict_min_version": "140.0", "update_url": "https://siemens.github.io/linux-entra-sso/thunderbird/updates.json", "data_collection_permissions": { "required": ["authenticationInfo"] } }, "gecko_android": { "strict_min_version": "142.0" } }, "action": { "default_popup": "popup/menu.html", "default_icon": "icons/linux-entra-sso.svg", "default_title": "Linux Entra SSO", "allowed_spaces": [ "mail", "calendar", "addressbook", "tasks", "settings", "default" ] }, "background": { "scripts": ["/src/background.js"], "type": "module" }, "permissions": [ "nativeMessaging", "webRequest", "webRequestBlocking", "storage" ], "host_permissions": ["https://login.microsoftonline.com/*"] } linux-entra-sso-1.11.0/platform/thunderbird/manifest.json.license000066400000000000000000000001211525176353400251270ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2025 Siemens SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.11.0/popup/000077500000000000000000000000001525176353400160205ustar00rootroot00000000000000linux-entra-sso-1.11.0/popup/menu.css000066400000000000000000000114411525176353400174770ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2024 Siemens AG */ :root { --font-size: 14px; --font-size-smaller: calc(var(--font-size) - 1px); --font-size-xsmall: calc(var(--font-size) - 3px); --font-size-larger: 15px; --font-family: Arial, Helvetica, Sans-Serif; --monospace-size: 12px; } /* * Colors from https://protocol.mozilla.org/docs/fundamentals/color.html */ :root { --light-gray-05: #ffffff; --light-gray-10: #f9f9fb; --light-gray-20: #f0f0f4; --light-gray-30: #e0e0e6; --light-gray-40: #cfcfd8; --light-gray-50: #bfbfc9; --dark-gray-05: #5b5b66; --dark-gray-20: #4a4a55; --dark-gray-40: #3a3944; --dark-gray-90: #15141a; --red-40: #ff6a75; --red-70: #c50042; --green-40: #54ffbd; --green-70: #008787; --orange-40: #ff8a50; --orange-70: #cc3d00; --blue-40: #0090ed; --blue-70: #054096; } /* set color scheme before JS overwrites it to avoid flickering of white page on dark default */ @media (prefers-color-scheme: light) { :root { --surface-0: var(--light-gray-05); --surface-1: var(--light-gray-20); --surface-2: var(--light-gray-40); } } @media (prefers-color-scheme: dark) { :root { --surface-0: var(--dark-gray-90); --surface-1: var(--dark-gray-40); --surface-2: var(--dark-gray-20); } } :root.light { --surface-0: var(--light-gray-05); --surface-1: var(--light-gray-20); --surface-2: var(--light-gray-40); --border-0: var(--light-gray-30); --text-0: var(--dark-gray-90); --text-1: var(--dark-gray-05); --text-red: var(--red-70); --text-green: var(--green-70); --text-orange: var(--orange-70); --text-blue: var(--blue-70); } :root.dark { --surface-0: var(--dark-gray-90); --surface-1: var(--dark-gray-40); --surface-2: var(--dark-gray-20); --border-0: var(--dark-gray-05); --text-0: var(--light-gray-05); --text-1: var(--light-gray-50); --text-red: var(--red-40); --text-green: var(--green-40); --text-orange: var(--orange-40); --text-blue: var(--blue-40); } html, body { width: 420px; padding: 0; margin: 0; } body { background-color: var(--surface-0); font-family: var(--font-family); } .entity { padding: 10px; margin: 10px; border-radius: 15px; color: var(--text-1); cursor: pointer; display: flow-root; } .info-no-trust { display: none; } body.has-account .info-no-trust { display: inline-block; } body.has-account .info-no-account { display: none; } body.pending .entity, body[data-app-state="loading"] .entity { cursor: wait; } body.nm-connected #nm-error-box { display: none; } .entity:hover { background-color: var(--surface-1); } .entity.active { background-color: var(--surface-2); color: var(--text-0); cursor: default; } .entity .avatar { float: left; margin-right: 10px; } .entity .info .name { font-size: var(--font-size-larger); } .entity .info .email { font-size: var(--font-size-smaller); } .entity .avatar img { width: 48px; height: 48px; } .state-connected-icon { color: var(--text-red); } .connected .state-connected-icon { color: var(--text-green); } .state-compliant-icon { color: var(--text-red); } .compliant .state-compliant-icon { color: var(--text-green); } .hidden { display: none !important; } .footer { padding-bottom: 5px; font-size: var(--font-size-smaller); min-height: 1em; color: var(--text-1); } .footer > .footer-block { border-top: 1px solid var(--border-0); padding-top: 5px; padding-bottom: 5px; margin: 0px 5px 0px 5px; } .footer > .footer-block > div { clear: both; } .footer a, .footer a:visited { color: var(--text-1); } .footer .right { float: right; } .footer .clear { clear: both; } .error-text { color: var(--text-red); } .footer .error-text a, .footer .error-text a:visited { color: var(--text-red); } .warning-text { color: var(--text-orange); } #status-text { color: var(--text-blue); } #status-text.error-text, body[data-app-state="failed"] #status-text { color: var(--text-red); } span.link { text-decoration: underline dotted; cursor: pointer; } #withdraw-access { display: none; } .connected #grant-access { display: none; } .connected #withdraw-access { display: inline-block; } #bg-sso-state.immutable .link { display: none; } /* show the immutable texts only when immutable */ #bg-sso-state .info-text-immutable { display: none; } #bg-sso-state.immutable .info-text-immutable.disabled { display: inline-block; } #bg-sso-state.immutable.connected .info-text-immutable.disabled { display: none; } #bg-sso-state.immutable.connected .info-text-immutable.enabled { display: inline-block; } linux-entra-sso-1.11.0/popup/menu.html000066400000000000000000000075401525176353400176600ustar00rootroot00000000000000
Avatar
Guest
linux-entra-sso-1.11.0/popup/menu.js000066400000000000000000000215261525176353400173300ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2024 Siemens AG */ /* max length of user-provided strings in UI */ const UI_MAX_STRING_LEN = 30; let bg_port = chrome.runtime.connect({ name: "linux-entra-sso" }); /* communication with backend is in progress */ let inflight = false; /* user is logged in */ let active = false; /* sso provider url */ let sso_url = null; /* current URL filter */ let current_filter = null; /* group policy update */ let gpo = null; /* size of avatar images */ const AVATAR_SIZE = 48; function set_inflight() { if (inflight) return false; inflight = true; annotate_body_if("pending", true); return true; } function clear_inflight() { inflight = false; annotate_body_if("pending", false); } function annotate_body_if(annotation, state) { if (state) document.body.classList.add(annotation); else document.body.classList.remove(annotation); } function annotate_by_id_if(element_id, annotation, state) { const element = document.getElementById(element_id); if (!element) return; if (state) element.classList.add(annotation); else element.classList.remove(annotation); } function setup_color_scheme() { const scheme = window?.matchMedia?.("(prefers-color-scheme:dark)")?.matches ? "dark" : "light"; document.documentElement.classList.add(scheme); } /** * If string is short enough, just set the innerText property. * If not, set it to a cropped version and set the title to the * full one. */ function set_text_cropped(element, str) { if (str.length <= UI_MAX_STRING_LEN) { element.innerText = str; element.title = ""; } else { element.innerText = str.slice(0, UI_MAX_STRING_LEN) + "…"; element.title = str; } } setup_color_scheme(); bg_port.onMessage.addListener(async (m) => { if (m.event == "stateChanged") { clear_inflight(); document.body.dataset.appState = m.app_state; annotate_body_if("has-account", m.accounts.length); annotate_body_if("nm-connected", m.nm_connected); if (m.ui_status) { document.getElementById("status-text").innerText = m.ui_status.text; } annotate_by_id_if("status-text", "error-text", m.ui_status?.error); annotate_by_id_if("status-box", "hidden", !m.ui_status); if (m.accounts !== null) { const accountsdom = document.getElementById("accountlist"); const entities = m.accounts.map((a) => create_account_entity(a)); accountsdom.replaceChildren(); entities.map((e) => accountsdom.appendChild(e)); } active = m.enabled && m.accounts.length; annotate_by_id_if("entity-guest", "active", !active); document.getElementById("broker-version").innerText = m.broker_version; /* show the app and host version */ let pvers = chrome.runtime.getManifest().version; let vstr = "v" + pvers; if (m.host_version !== null && m.host_version !== pvers) { vstr += " (host v" + m.host_version + ")"; } document.getElementById("version").innerText = vstr; if (m.device) { set_text_cropped( document.getElementById("device-name"), m.device.name, ); document.getElementById("state-compliant-value").innerText = m .device.compliant ? "compliant" : "not compliant"; annotate_body_if("compliant", m.device.compliant); } annotate_by_id_if("bg-device-state", "hidden", !m.device); sso_url = m.sso_url; gpo = m.gpo_update; check_sso_provider_perms(); check_bg_sso_enabled(); check_gpo_update(); } }); function create_account_entity(account) { const entity = document.createElement("div"); entity.classList.add("entity"); if (account.active) entity.classList.add("active"); const avatarDiv = document.createElement("div"); avatarDiv.classList.add("avatar"); if (account.avatar !== null) { const canvas = document.createElement("canvas"); canvas.width = AVATAR_SIZE; canvas.height = AVATAR_SIZE; const ctx = canvas.getContext("2d"); let img = new Image(AVATAR_SIZE, AVATAR_SIZE); img.src = account.avatar; img.onload = () => { ctx.drawImage(img, 0, 0); }; avatarDiv.appendChild(canvas); } else { const fallbackImg = document.createElement("img"); fallbackImg.src = "profile-outline.svg"; fallbackImg.width = AVATAR_SIZE; fallbackImg.height = AVATAR_SIZE; fallbackImg.alt = "Avatar"; avatarDiv.appendChild(fallbackImg); } entity.appendChild(avatarDiv); const infoDiv = document.createElement("div"); infoDiv.classList.add("info"); const nameDiv = document.createElement("div"); nameDiv.className = "name"; nameDiv.innerText = account.name; infoDiv.appendChild(nameDiv); const emailDiv = document.createElement("div"); emailDiv.className = "email"; emailDiv.innerText = account.username; infoDiv.appendChild(emailDiv); entity.appendChild(infoDiv); entity.addEventListener("click", (event) => { if (account.active) return; if (!set_inflight(this)) return; bg_port.postMessage({ command: "enable", username: account.username }); }); return entity; } document.getElementById("entity-guest").addEventListener("click", (event) => { if (!active) return; if (!set_inflight(this)) return; bg_port.postMessage({ command: "disable" }); }); function check_sso_provider_perms() { const permissionsToCheck = { origins: [sso_url + "/*"], }; chrome.permissions.contains(permissionsToCheck).then((result) => { annotate_by_id_if("message-box", "hidden", result); }); } async function check_bg_sso_enabled() { const [tab] = await chrome.tabs.query({ currentWindow: true, active: true, }); if ( !tab?.url || !tab.url.startsWith("https://") || URL.parse(tab.url)?.origin === URL.parse(sso_url).origin ) { annotate_by_id_if("bg-sso-state", "hidden", true); return; } annotate_by_id_if("bg-sso-state", "hidden", false); const tab_hostname = new URL(tab.url).hostname; current_filter = "https://" + tab_hostname + "/*"; set_text_cropped(document.getElementById("current-url"), tab_hostname); const permissionsToCheck = { origins: [current_filter], }; chrome.permissions.contains(permissionsToCheck).then((result) => { annotate_by_id_if("bg-sso-state", "connected", result); }); const state_immutable = gpo !== null && (gpo.has_catch_all || tab_hostname in gpo.apps_managed); annotate_by_id_if("bg-sso-state", "immutable", state_immutable); } function check_gpo_update() { annotate_by_id_if("gpo-update-box", "hidden", gpo === null || !gpo.pending); } function apply_gpo_update() { if (gpo === null) return; request_host_permission(gpo.filters_to_add); remove_host_permission(gpo.filters_to_remove); } function request_host_permission(urls) { if (urls === null || urls.length == 0) return; const permissionsToRequest = { origins: urls, }; chrome.permissions.request(permissionsToRequest).then((granted) => { if (granted) { console.log("Permission granted"); // No need to update the UI as this will trigger the permission // changed event in the background script, which triggers an // UI update. } else { console.log("Failed to get permission"); } }); // The permission-request window might open below the webextensions panel. // This has been observed on Thunderbird 128. Close the panel, so the user // can grant the permission. window.close(); } function remove_host_permission(urls) { if (urls === null || urls.length == 0) return; const permissionsToRemove = { origins: urls, }; chrome.permissions.remove(permissionsToRemove).then((removed) => { if (removed) console.log("Permission removed"); else console.log("Failed to remove permission"); }); } // Requires user interaction, as otherwise we lack the permission to // request further host permissions document.getElementById("grant-access").addEventListener("click", (event) => { request_host_permission([current_filter]); }); document .getElementById("withdraw-access") .addEventListener("click", (event) => { remove_host_permission([current_filter]); }); document .getElementById("grant-access-sso") .addEventListener("click", (event) => { request_host_permission([sso_url + "/*"]); }); document .getElementById("apply-gpo-update") .addEventListener("click", (event) => { apply_gpo_update(); }); linux-entra-sso-1.11.0/renovate.json5000066400000000000000000000024371525176353400174660ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright 2026 Siemens AG // SPDX-License-Identifier: MPL-2.0 { $schema: "https://docs.renovatebot.com/renovate-schema.json", extends: [ "config:best-practices", "helpers:pinGitHubActionDigestsToSemver", "group:all", "schedule:monthly", ":semanticCommits", ":semanticCommitTypeAll(chore)", ":prHourlyLimitNone", ":prConcurrentLimitNone", ], minimumReleaseAge: '3 days', lockFileMaintenance: { enabled: false, }, assigneesFromCodeOwners: true, separateMajorMinor: false, customManagers: [ { customType: "regex", managerFilePatterns: ["/^\\.github/workflows/[^/]+\\.ya?ml$/"], matchStrings: ["WEB_EXT_VERS:\\s*(?\\S+)"], depNameTemplate: "web-ext", datasourceTemplate: "npm", }, { customType: "regex", managerFilePatterns: ["/^\\.github/workflows/[^/]+\\.ya?ml$/"], matchStrings: ["CRX3_VERS:\\s*(?\\S+)"], depNameTemplate: "crx3", datasourceTemplate: "npm", }, { customType: "regex", managerFilePatterns: ["/^\\.github/workflows/[^/]+\\.ya?ml$/"], matchStrings: ["CWS_UPLOAD_VERS:\\s*(?\\S+)"], depNameTemplate: "chrome-webstore-upload", datasourceTemplate: "npm", }, ], } linux-entra-sso-1.11.0/src/000077500000000000000000000000001525176353400154445ustar00rootroot00000000000000linux-entra-sso-1.11.0/src/account.js000066400000000000000000000336351525176353400174500ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { getLogger, load_icon } from "./utils.js"; import { StateMachine } from "./state-machine.js"; const log = getLogger("accounts"); /* refresh the token if only x time is left */ const TOKEN_MIN_VALIDITY_MS = 60 * 1000; /* * Whether the user wants SSO. UNKNOWN means that no explicit choice was * recorded yet, which counts as active. */ export const SsoState = Object.freeze({ UNKNOWN: "unknown", LOGGED_IN: "logged-in", LOGGED_OUT: "logged-out", }); const SSO_TRANSITIONS = Object.freeze({ [SsoState.UNKNOWN]: [SsoState.LOGGED_IN, SsoState.LOGGED_OUT], [SsoState.LOGGED_IN]: [SsoState.LOGGED_OUT], [SsoState.LOGGED_OUT]: [SsoState.LOGGED_IN], }); class SsoStateMachine extends StateMachine { constructor() { super("sso-state", SSO_TRANSITIONS, SsoState.UNKNOWN); } is_active() { return !this.is_in(SsoState.LOGGED_OUT); } log_in() { return this.transition(SsoState.LOGGED_IN); } log_out() { return this.transition(SsoState.LOGGED_OUT); } } /* * Where the registered accounts come from. UNKNOWN means that we did not * determine it yet. PROVISIONAL data is the last-known set restored from * disk, which carries no tokens and is replaced once the broker answers. */ export const AccountsState = Object.freeze({ UNKNOWN: "unknown", PROVISIONAL: "provisional", AUTHORITATIVE: "authoritative", }); const ACCOUNTS_TRANSITIONS = Object.freeze({ [AccountsState.UNKNOWN]: [ AccountsState.PROVISIONAL, AccountsState.AUTHORITATIVE, ], [AccountsState.PROVISIONAL]: [AccountsState.AUTHORITATIVE], [AccountsState.AUTHORITATIVE]: [], }); class AccountsStateMachine extends StateMachine { constructor() { super("accounts-state", ACCOUNTS_TRANSITIONS, AccountsState.UNKNOWN); } is_authoritative() { return this.is_in(AccountsState.AUTHORITATIVE); } is_provisional() { return this.is_in(AccountsState.PROVISIONAL); } restored_from_disk() { return this.transition(AccountsState.PROVISIONAL); } confirmed_by_broker() { return this.transition(AccountsState.AUTHORITATIVE); } } export class Account { #broker_obj = null; /* ImageData cache for the tray icon (not serialized) */ #avatar_imgdata = null; /* circular avatar as a serializable data URL; null => default icon */ avatar = null; active = false; access_token = null; access_token_exp = 0; constructor(broker_obj) { this.#broker_obj = { ...broker_obj }; } name() { return this.#broker_obj.name; } username() { return this.#broker_obj.username; } brokerObject() { return this.#broker_obj; } toMenuObject() { return { name: this.name(), username: this.username(), avatar: this.avatar, active: this.active, }; } async getAvatarImgData() { if (this.#avatar_imgdata) { return this.#avatar_imgdata; } if (!this.avatar) { this.#avatar_imgdata = await load_icon( "/icons/profile-outline_48.png", 48, ); return this.#avatar_imgdata; } /* derive ImageData from the serializable data URL */ const bitmap = await createImageBitmap( await (await fetch(this.avatar)).blob(), ); const canvas = new OffscreenCanvas(48, 48); const ctx = canvas.getContext("2d"); ctx.drawImage(bitmap, 0, 0, 48, 48); this.#avatar_imgdata = ctx.getImageData(0, 0, 48, 48); return this.#avatar_imgdata; } setAvatar(dataUrl) { this.avatar = dataUrl; /* clear cache, will be rebuild on next getAvatarImgData */ this.#avatar_imgdata = null; } async getDecoratedAvatar(width) { let imgdata = await this.getAvatarImgData(); const sWidth = imgdata.width; const lineWidth = Math.min(2, width / 12); let buffer = new OffscreenCanvas(sWidth, sWidth); let ctx_buffer = buffer.getContext("2d"); ctx_buffer.putImageData(imgdata, 0, 0); let canvas = new OffscreenCanvas(width, width); let ctx = canvas.getContext("2d"); ctx.save(); ctx.beginPath(); ctx.arc(width / 2, width / 2, width / 2, 0, Math.PI * 2, false); ctx.clip(); ctx.drawImage(buffer, 0, 0, sWidth, sWidth, 0, 0, width, width); ctx.restore(); return ctx.getImageData(0, 0, width, width); } toSerial(with_secrets = false) { const serial = { broker_obj: this.brokerObject(), active: this.active, avatar: this.avatar, }; if (with_secrets) { serial.access_token = this.access_token; serial.access_token_exp = this.access_token_exp; } return serial; } static fromSerial(serial) { let acc = new Account(serial.broker_obj); acc.active = serial.active; acc.avatar = serial.avatar ?? null; acc.access_token = serial.access_token ?? null; acc.access_token_exp = serial.access_token_exp ?? 0; return acc; } } export class AccountManager { #registered = []; #accounts = new AccountsStateMachine(); #sso = new SsoStateMachine(); /* in-flight token requests, keyed by username, to dedup concurrent calls */ #token_requests = new Map(); hasAccounts() { return this.#registered.length != 0; } /** * @returns if we got account data from the broker */ hasBrokerData() { return this.#accounts.is_authoritative(); } /** * @returns if the accounts are the last-known set from disk, which is * used until the broker confirms the real one */ hasProvisionalData() { return this.#accounts.is_provisional(); } getActive() { return this.#registered.find((a) => a.active); } /** * @returns if SSO is active (i.e. the user did not explicitly log out) */ isActive() { return this.#sso.is_active(); } setActive(active) { if (active) this.#sso.log_in(); else this.#sso.log_out(); } getRegistered() { return this.#registered; } logout() { this.#registered.map((a) => (a.active = false)); } selectAccount(username) { if (!username) { let account = this.#registered[0]; this.logout(); account.active = true; return account; } const account = this.#registered.find((a) => a.username() == username); if (account === undefined) { log.warn("no account found with username " + username); return undefined; } this.logout(); account.active = true; return account; } async loadAccounts(broker) { if (this.hasBrokerData()) return; const _accounts = await broker.getAccounts(); if (!_accounts?.length) { this.#registered = []; /* an empty result is still an answer: no account is registered */ if (_accounts) this.#accounts.confirmed_by_broker(); return; } // remember the current selection and avatars before replacing the // accounts with the freshly queried ones. const last_username = this.getActive()?.username(); const previous_avatars = new Map( this.#registered.map((a) => [a.username(), a.avatar]), ); /* we successfully got account data from the broker */ this.#registered = _accounts; this.#accounts.confirmed_by_broker(); // carry over the avatars so the UI does not flash the default icon // while the profile pictures are refetched below. for (const account of this.#registered) { account.avatar = previous_avatars.get(account.username()) ?? null; } // only auto-select an account if the user did not explicitly disable SSO if (!this.isActive()) { log.info("SSO is disabled, not selecting an account"); } else if (last_username && this.selectAccount(last_username)) { log.info( "select previously used account: " + this.getActive().username(), ); } else { this.selectAccount(); log.info("select first account: " + this.getActive().username()); } await Promise.all( this.#registered.map((a) => this.loadProfilePicture(broker, a)), ); } async getToken(broker, account) { if (Date.now() + TOKEN_MIN_VALIDITY_MS < account.access_token_exp) { return account.access_token; } const username = account.username(); /* coalesce concurrent requests for the same account */ let request = this.#token_requests.get(username); if (request) { return request; } request = this.#acquireToken(broker, account); this.#token_requests.set(username, request); try { return await request; } finally { this.#token_requests.delete(username); } } async #acquireToken(broker, account) { try { const graph_token = await broker.acquireTokenSilently(account); log.info("API token acquired for " + account.username()); account.access_token = graph_token.accessToken; account.access_token_exp = graph_token.expiresOn; return account.access_token; } catch (error) { log.error( "failed to acquire API token for " + account.username(), error, ); /* do not keep a token the broker refused to renew */ account.access_token = null; account.access_token_exp = 0; return null; } } async loadProfilePicture(broker, account) { const graph_token = await this.getToken(broker, account); if (!graph_token) return; const response = await fetch( "https://graph.microsoft.com/v1.0/me/photos/48x48/$value", { headers: { Accept: "image/jpeg", Authorization: "Bearer " + graph_token, }, }, ); if (response.ok) { let avatar = await createImageBitmap(await response.blob()); let canvas = new OffscreenCanvas(48, 48); let ctx = canvas.getContext("2d"); ctx.beginPath(); ctx.arc(24, 24, 24, 0, Math.PI * 2, false); ctx.clip(); ctx.drawImage(avatar, 0, 0, 48, 48); /* serialize image to data URL (ugly, but portable) */ let blob = await canvas.convertToBlob(); const dataUrl = await new Promise((r) => { let a = new FileReader(); a.onload = r; a.readAsDataURL(blob); }).then((e) => e.target.result); account.setAvatar(dataUrl); } else { log.warn("could not get profile picture of " + account.username()); } } /* * Store the current state in the local storage. * To not leak account data in disabled state, we clear the account object. */ async persist() { if (!this.hasAccounts()) return; const ssostate = { state: this.isActive(), accounts: this.getActive() ? this.#registered.map((a) => a.toSerial()) : [], }; const appstate = { broker_queried: this.hasBrokerData(), accounts: this.#registered.map((a) => a.toSerial(true)), }; return Promise.all([ chrome.storage.local.set({ ssostate }), chrome.storage.session.set({ account_manager: appstate }), ]); } /* * Drop account data cached on disk, but keep the logged-out marker. */ async #wipeCachedAccounts() { return chrome.storage.local.set({ ssostate: { state: false, accounts: [] }, }); } async restore() { const [data, sessionData] = await Promise.all([ chrome.storage.local.get("ssostate"), chrome.storage.session.get("account_manager"), ]); if (sessionData.account_manager) { this.#registered = sessionData.account_manager.accounts.map((a) => Account.fromSerial(a), ) ?? []; if (sessionData.account_manager.broker_queried) { this.#accounts.confirmed_by_broker(); } else if (this.#registered.length > 0) { this.#accounts.restored_from_disk(); } } /* restored from session */ if (this.#registered.length > 0) { this.setActive(this.getActive() != null); return; } /* no accounts in session, try restore from local storage */ if (!data.ssostate) { log.info("no preserved state found"); // if the SSO is not explicitly disabled, we assume it is on. return; } const state_active = data.ssostate.state; if (!state_active) { this.setActive(false); await this.#wipeCachedAccounts(); return; } if (data.ssostate.accounts) { this.#registered = data.ssostate.accounts.map((a) => Account.fromSerial(a), ); if (this.#registered.length > 0) { this.#accounts.restored_from_disk(); } } this.setActive(true); const active_acc = this.getActive(); if (active_acc) { log.info( "temporarily using last-known account: " + active_acc.username(), ); } } } linux-entra-sso-1.11.0/src/app-state.js000066400000000000000000000050341525176353400177020ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2026 Siemens */ import { Deferred } from "./utils.js"; import { StateMachine } from "./state-machine.js"; /* * Top-level application state. Bootstrapping is the first phase covered * here, further states are meant to be added as substates. * * NEW -> RESTORING -> WAITING_FOR_BROKER -> LOADING -> COMPLETE * ^ | * | v * +------- FAILED */ export const AppState = Object.freeze({ NEW: "new", RESTORING: "restoring", WAITING_FOR_BROKER: "waiting-for-broker", LOADING: "loading", COMPLETE: "complete", FAILED: "failed", }); /* Successor states, any transition not listed here is rejected. */ const TRANSITIONS = Object.freeze({ [AppState.NEW]: [AppState.RESTORING], [AppState.RESTORING]: [AppState.WAITING_FOR_BROKER], [AppState.WAITING_FOR_BROKER]: [AppState.LOADING], [AppState.LOADING]: [AppState.COMPLETE, AppState.FAILED], [AppState.COMPLETE]: [], /* a failed bootstrap can be retried */ [AppState.FAILED]: [AppState.LOADING], }); export class AppStateMachine extends StateMachine { #broker_ready = new Deferred(); constructor() { super("app-state", TRANSITIONS, AppState.NEW); } /* ---- conditions ---- */ /* The persisted state is available, so the UI can be updated. */ is_restored() { return !this.is_in(AppState.NEW, AppState.RESTORING); } may_bootstrap() { return this.can_enter(AppState.LOADING); } has_failed() { return this.is_in(AppState.FAILED); } /* ---- transitions ---- */ /* Returns true only for the first caller, which owns the startup. */ initialize() { return this.transition(AppState.RESTORING); } restored() { return this.transition(AppState.WAITING_FOR_BROKER); } /* Opens the gate that holds back begin_bootstrap() until the host reported a state. */ broker_ready() { this.#broker_ready.resolve(); } /* Resolves to true if the caller may run the bootstrap sequence. */ async begin_bootstrap() { if (!this.may_bootstrap()) return false; await this.#broker_ready.promise; return this.transition(AppState.LOADING); } bootstrap_succeeded() { return this.transition(AppState.COMPLETE); } bootstrap_failed() { return this.transition(AppState.FAILED); } } linux-entra-sso-1.11.0/src/background.js000066400000000000000000000176671525176353400201420ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2024 Siemens AG */ import { create_platform } from "./platform-factory.js"; import { Broker } from "./broker.js"; import { AccountManager } from "./account.js"; import { getLogger } from "./utils.js"; import { PolicyManager } from "./policy.js"; import { DeviceManager } from "./device.js"; import { AppStateMachine } from "./app-state.js"; const log = getLogger("app"); const PLATFORM = create_platform(); let broker = null; let policyManager = null; let accountManager = null; let deviceManager = null; let port_menu = null; const app_state = new AppStateMachine(); /* status messages to surface in the UI, keyed by the reporting source */ const status_by_source = new Map(); /* * Check if all conditions for SSO are met */ function is_operational() { return Boolean( !is_in_error_state() && accountManager.isActive() && accountManager.getActive(), ); } /* * Update the status message of a source. Pass a null text to withdraw it, * which leaves messages of other sources untouched. */ function report_status(source, text, is_error = false) { if (text) status_by_source.set(source, { text, error: is_error }); else status_by_source.delete(source); notify_state_change(true); } /* The status shown in the UI, errors take precedence over progress messages. */ function current_status() { const all = [...status_by_source.values()]; return all.find((s) => s.error) ?? all[0] ?? null; } function is_in_error_state() { return broker.hasConnectionError() || app_state.has_failed(); } async function on_permissions_changed() { log.info("permissions changed, reload host_permissions"); await PLATFORM.update_host_permissions(); notify_state_change(); } /* * Update the UI according to the current state */ async function update_tray(action_needed) { chrome.action.enable(); chrome.action.setBadgeText({ text: action_needed ? "1" : null, }); if (is_operational()) { const account = accountManager.getActive(); const imgdata = {}; let icon_title = account.username(); // shorten the title a bit icon_title = PLATFORM.transform_ui_title(icon_title); chrome.action.setTitle({ title: icon_title, }); for (const r of [16, 32, 48]) { imgdata[r] = await account.getDecoratedAvatar(r); } chrome.action.setIcon({ imageData: imgdata, }); return; } /* inactive states */ PLATFORM.setIconDisabled(); let title = "EntraID SSO disabled"; if (accountManager.isActive()) title = "EntraID SSO disabled (waiting for broker)"; if (accountManager.hasAccounts() == 0) { title = "EntraID SSO disabled (no accounts registered)"; } if (!broker.isConnected()) { title = "EntraID SSO disabled (no connection to host application)"; } // We have limited space on Thunderbird, hence shorten the title title = PLATFORM.transform_ui_title(title); chrome.action.setTitle({ title: title }); } /* * Update the tray icon, (un)register the handlers and notify * the menu about a state change. */ function notify_state_change(ui_only = false) { // on service worker startup, delay all updates until we restored // the application state from storage. if (!app_state.is_restored()) return; const gpo_update = policyManager.getPolicyUpdate( PLATFORM.well_known_app_filters, ); const ui_status = current_status(); let action_needed = !PLATFORM.sso_url_permitted || gpo_update.pending || Boolean(ui_status?.error) || is_in_error_state(); update_tray(action_needed); if (!ui_only && broker.isConnected()) { log.debug("update handlers"); PLATFORM.update_request_handlers( is_operational(), accountManager.getActive(), broker, ); } if (port_menu === null) return; deviceManager.updateDeviceInfo(broker).then((updated) => { /* only notify on success to avoid indefinite recursion as errors are not cached */ if (updated) { deviceManager.persist(); notify_state_change(true); } }); port_menu.postMessage({ event: "stateChanged", accounts: accountManager.getRegistered().map((a) => a.toMenuObject()), nm_connected: broker.isConnected(), device: deviceManager.getDevice(), enabled: accountManager.isActive(), host_version: PLATFORM.host_versions.native, broker_version: PLATFORM.host_versions.broker, sso_url: PLATFORM.getSsoUrl(), gpo_update: gpo_update, ui_status: ui_status, app_state: app_state.state, }); } async function on_message_menu(request) { if (is_in_error_state()) { notify_state_change(true); return; } if (request.command == "enable") { accountManager.setActive(true); const account = accountManager.selectAccount(request.username); if (account) log.info("select account " + account.username()); } else if (request.command == "disable") { accountManager.setActive(false); accountManager.logout(); } accountManager.persist(); notify_state_change(); } async function on_broker_state_change(online) { if (online) { log.debug("DBus broker is online"); } else { log.debug("DBus broker is offline"); } // unblock the initial data loading once the host reported a state. app_state.broker_ready(); notify_state_change(true); } async function bootstrap_from_broker() { if (!(await app_state.begin_bootstrap())) return; report_status("bootstrap", "Loading data from broker\u2026"); try { await accountManager.loadAccounts(broker); accountManager.persist(); await deviceManager.loadDeviceInfo(broker); deviceManager.persist(); await PLATFORM.setup(broker); app_state.bootstrap_succeeded(); report_status("bootstrap", null); } catch (error) { app_state.bootstrap_failed(); report_status( "bootstrap", "Failed to load data from broker: " + error, true, ); } notify_state_change(); } async function on_storage_changed(_changes, areaName) { if (areaName == "managed") { await policyManager.load_policies(); } } function on_startup() { if (!app_state.initialize()) { log.debug("linux-entra-sso already initialized"); return; } log.info("start linux-entra-sso on " + PLATFORM.browser); PLATFORM.set_status_handler((text, is_error) => report_status("platform", text, is_error), ); policyManager = new PolicyManager(); chrome.storage.onChanged.addListener(on_storage_changed); chrome.permissions.onAdded.addListener(on_permissions_changed); chrome.permissions.onRemoved.addListener(on_permissions_changed); broker = new Broker("linux_entra_sso", on_broker_state_change); accountManager = new AccountManager(broker); deviceManager = new DeviceManager(accountManager); Promise.all([ PLATFORM.update_host_permissions(), policyManager.load_policies(), accountManager.restore(), deviceManager.restore(), broker.restore(), ]).then(() => { app_state.restored(); notify_state_change(); /* asynchronously load external state */ broker.connect(); bootstrap_from_broker(); }); chrome.runtime.onConnect.addListener((port) => { port_menu = port; port_menu.onMessage.addListener(on_message_menu); port_menu.onDisconnect.addListener(() => { port_menu = null; }); broker.connect(); bootstrap_from_broker(); notify_state_change(true); }); } // use this API to prevent the extension from being disabled chrome.runtime.onStartup.addListener(on_startup); on_startup(); linux-entra-sso-1.11.0/src/broker.js000066400000000000000000000216421525176353400172730ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { getLogger, Deferred } from "./utils.js"; import { Account } from "./account.js"; const log = getLogger("broker"); /** * Queue to resolve promises, once the data arrives from the * remote backend. */ export class RpcHandlerQueue { static DEFAULT_TIMEOUT_MS = 15 * 1000; #queue = []; register_handle(id, timeout_ms = RpcHandlerQueue.DEFAULT_TIMEOUT_MS) { const handle = { id: id, dfd: new Deferred(), }; this.#queue.push(handle); const timeout = new Promise((_, reject) => setTimeout(() => { this.#drop_handle(handle); reject(`timeout while waiting for ${id} response`); }, timeout_ms), ); return Promise.race([handle.dfd.promise, timeout]); } resolve_handle(id, data) { this.#take_handle(id)?.dfd.resolve(data); } reject_handle(id, data) { this.#take_handle(id)?.dfd.reject(data); } /* Fail all outstanding requests, e.g. when the transport went away. */ reject_all(data) { const pending = this.#queue; this.#queue = []; for (const hdl of pending) { hdl.dfd.reject(data); } } has_pending() { return this.#queue.length != 0; } /* Take the oldest handle for that id, as responses arrive in order. */ #take_handle(id) { const idx = this.#queue.findIndex((hdl) => hdl.id == id); if (idx === -1) return null; return this.#queue.splice(idx, 1)[0]; } #drop_handle(handle) { const idx = this.#queue.indexOf(handle); if (idx !== -1) this.#queue.splice(idx, 1); } } export class Broker { static IDLE_DISCONNECT_MS = 10 * 1000; #name = null; #notify_fn = null; #port_native = null; #rpc_queue = new RpcHandlerQueue(); /* track if the NM connection was successful */ #conn_error = false; /* track if we ever had a successful connection to the native app */ #had_connection = false; #idle_timer = null; constructor(name, state_change_fn) { this.#name = name; this.#notify_fn = state_change_fn; } connect() { if (this.#port_native) { this.#reset_idle_timer(); return; } this.#conn_error = false; this.#port_native = chrome.runtime.connectNative(this.#name); this.#port_native.onDisconnect.addListener(() => { /* note, that this is not called on .disconnect(), only on errors */ this.#port_native = null; if (chrome.runtime.lastError) { log.error( "error in native application connection: " + chrome.runtime.lastError.message, ); this.#conn_error = true; } else { /* Connection closed by the native application. */ log.error("native application connection closed"); } this.#rpc_queue.reject_all("lost connection to native application"); this.#notify_fn(false); }); this.#port_native.onMessage.addListener( this.#on_message_native.bind(this), ); this.#reset_idle_timer(); } disconnect() { this.#clear_idle_timer(); if (!this.#port_native) return; this.#port_native.disconnect(); this.#port_native = null; } /** * Ensure we are connected to the broker and (re)start the * inactivity timer. Must be called by every broker function. */ #keep_alive() { this.connect(); this.#reset_idle_timer(); } #reset_idle_timer() { this.#clear_idle_timer(); /* keep the connection alive to prevent the worker from shutting down */ this.#idle_timer = setTimeout(() => { this.#idle_timer = null; /* never tear down the port while a response is still outstanding */ if (this.#rpc_queue.has_pending()) { this.#reset_idle_timer(); return; } log.debug("disconnecting from host tooling after inactivity"); this.disconnect(); }, Broker.IDLE_DISCONNECT_MS); } #clear_idle_timer() { if (this.#idle_timer !== null) { clearTimeout(this.#idle_timer); this.#idle_timer = null; } } isConnected() { /** * As we internally manage the lifecycle of the connection, * we only let the caller know if we are unable to connect to the host */ return !this.#conn_error && this.#had_connection; } /** * @returns if connecting to the native application failed. Unlike * isConnected(), this is false while the connection is not established yet. */ hasConnectionError() { return this.#conn_error; } /* * Persist the connection tracking state in the session storage. */ async persist() { return chrome.storage.session.set({ broker_state: { had_connection: this.#had_connection }, }); } async restore() { const data = await chrome.storage.session.get("broker_state"); if (!data.broker_state) return; this.#had_connection = data.broker_state.had_connection ?? false; } getAccounts() { this.#keep_alive(); this.#port_native.postMessage({ command: "getAccounts" }); return this.#rpc_queue.register_handle("getAccounts"); } async acquireTokenSilently(account) { this.#keep_alive(); this.#port_native.postMessage({ command: "acquireTokenSilently", account: account.brokerObject(), }); return this.#rpc_queue.register_handle("acquireTokenSilently"); } async acquirePrtSsoCookie(account, ssoUrl) { this.#keep_alive(); this.#port_native.postMessage({ command: "acquirePrtSsoCookie", account: account.brokerObject(), ssoUrl: ssoUrl, }); return this.#rpc_queue.register_handle("acquirePrtSsoCookie"); } async getVersion() { this.#keep_alive(); this.#port_native.postMessage({ command: "getVersion" }); return this.#rpc_queue.register_handle("getVersion"); } /* Normalize the error from the native message into a readable string. */ #stringify_error(error) { if (error !== null && typeof error === "object") { return JSON.stringify(error); } return error; } #on_message_native(response) { /* receiving any message proves the connection was successful */ if (!this.#had_connection) { this.#had_connection = true; this.persist(); log.info("connected to host tooling"); } /* handle events (not an RPC response) */ if (response.command == "brokerStateChanged") { this.#notify_fn(response.message == "online"); return; } /* on rpc messages, reject all responses that have errors */ if ("error" in response.message) { this.#rpc_queue.reject_handle( response.command, this.#stringify_error(response.message.error), ); return; } if (response.command == "acquirePrtSsoCookie") { var cookieData = response.message; /* microsoft-identity-broker > 2.0.1 */ if ("cookieItems" in cookieData) { cookieData = cookieData.cookieItems[0]; } this.#rpc_queue.resolve_handle("acquirePrtSsoCookie", { cookieName: cookieData.cookieName, cookieContent: cookieData.cookieContent, }); } else if (response.command == "getAccounts") { let _accounts = []; for (const a of response.message.accounts) { _accounts.push(new Account(a)); } this.#rpc_queue.resolve_handle("getAccounts", _accounts); } else if (response.command == "getVersion") { this.#rpc_queue.resolve_handle("getVersion", { native: response.message.native, broker: response.message.linuxBrokerVersion, }); } else if (response.command == "acquireTokenSilently") { if ("error" in response.message.brokerTokenResponse) { this.#rpc_queue.reject_handle( "acquireTokenSilently", this.#stringify_error( response.message.brokerTokenResponse.error, ), ); } else { this.#rpc_queue.resolve_handle("acquireTokenSilently", { ...response.message.brokerTokenResponse, }); } } else { log.warn("unknown command: " + response.command); } } } linux-entra-sso-1.11.0/src/device.js000066400000000000000000000077571525176353400172610ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { getLogger, jwt_get_payload } from "./utils.js"; const log = getLogger("device"); export class Device { name = null; compliant = null; constructor(name, compliant) { this.name = name; this.compliant = compliant; } toSerial() { return { name: this.name, compliant: this.compliant, }; } static fromSerial(serial) { return new Device(serial.name, serial.compliant); } } export class DeviceManager { static DEVICE_REFRESH_INTERVAL_MIN_MS = 30 * 60 * 1000; #am = null; #last_refresh = 0; /* in-flight device load, to dedup concurrent calls */ #refresh_promise = null; device = null; constructor(account_manager) { this.#am = account_manager; this.device = null; } /** * Update the device information if not recent enough. Subsequent calls * are cheap as the device information is fetched from the cache. Callers * must not immediately call this again in case the function returns false, * as error states are not cached (to allow recovering after sporadic errors). * @returns true if successfully updated */ async updateDeviceInfo(broker) { if ( Date.now() < this.#last_refresh + DeviceManager.DEVICE_REFRESH_INTERVAL_MIN_MS ) { return false; } return await this.loadDeviceInfo(broker); } /** * Load information about the accessing device (e.g. compliance state) * @returns true on success */ async loadDeviceInfo(broker) { /* coalesce concurrent loads into a single request */ if (this.#refresh_promise) { return this.#refresh_promise; } this.#refresh_promise = this.#loadDeviceInfo(broker); try { return await this.#refresh_promise; } finally { this.#refresh_promise = null; } } async #loadDeviceInfo(broker) { if (!this.#am.hasAccounts()) { return false; } const graph_token = await this.#am.getToken( broker, this.#am.getRegistered()[0], ); if (!graph_token) { return false; } const grants = jwt_get_payload(graph_token); if (!grants["deviceid"]) { log.warn("access token does not have deviceid grant"); return false; } const response = await fetch( `https://graph.microsoft.com/v1.0/devices(deviceId='{${grants["deviceid"]}}')?$select=isCompliant,displayName`, { headers: { Accept: "application/json", Authorization: "Bearer " + graph_token, }, }, ); if (!response.ok) { log.error( `failed to query device state: HTTP ${response.status} ${response.statusText}`, ); return false; } const data = await response.json(); this.#last_refresh = Date.now(); this.device = new Device(data.displayName, data.isCompliant); log.info("updated device information"); return true; } getDevice() { return this.device; } /* * Store the current device state in the session storage. */ async persist() { const state = { last_refresh: this.#last_refresh, device: this.device ? this.device.toSerial() : null, }; return chrome.storage.session.set({ device_manager: state }); } async restore() { const data = await chrome.storage.session.get("device_manager"); if (!data.device_manager) { return; } this.#last_refresh = data.device_manager.last_refresh ?? 0; this.device = data.device_manager.device ? Device.fromSerial(data.device_manager.device) : null; } } linux-entra-sso-1.11.0/src/platform.js000066400000000000000000000062401525176353400176300ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { getLogger } from "./utils.js"; const log = getLogger("platform"); export class Platform { static SSO_URL = "https://login.microsoftonline.com"; browser; host_versions = { native: null, broker: null, }; /* references needed for PRT injection */ account = null; well_known_app_filters = []; sso_url_permitted = true; /* invoked with (text, is_error) to surface platform issues in the UI */ #status_handler = null; constructor() { /* * The WebRequest API operates on allowed URLs only. * To intercept a sub-resource request (e.g. from an iframe), the extension * must have access to both the requested URL and its initiator. */ this.well_known_app_filters = [Platform.SSO_URL + "/*"]; } /** * Load platform information from backend. */ async setup(broker) { // If we already know the versions for this session (restored from // session storage), do not query the broker again: getVersion is a // broker RPC that would re-activate the broker via D-Bus. await this.#restore(); if (this.host_versions.native !== null) { return; } this.host_versions = await broker.getVersion(); await this.#persist(); } /* * Persist the host and broker versions in the session storage. */ async #persist() { return chrome.storage.session.set({ host_versions: this.host_versions, }); } async #restore() { const data = await chrome.storage.session.get("host_versions"); if (!data.host_versions) return; this.host_versions = data.host_versions; } setIconDisabled() { chrome.action.setIcon({ path: { 48: "/icons/linux-entra-sso_48.png", 128: "/icons/linux-entra-sso_128.png", }, }); } /** * Can be overwritten to shorten the title on platforms that print the * title next to the icon (instead of in a tooltip). */ transform_ui_title(title) { return title; } getSsoUrl() { return Platform.SSO_URL; } set_status_handler(handler) { this.#status_handler = handler; } /* Surface a platform error in the UI (action badge and menu message). */ report_error(text) { log.error(text); this.#status_handler?.(text, true); } /* Withdraw a previously reported error. */ clear_error() { this.#status_handler?.(null); } update_request_handlers(enabled, account, broker) { this.account = account; } async update_host_permissions() { const currentPermissions = await chrome.permissions.getAll(); this.well_known_app_filters = currentPermissions.origins; // check if we have access to the SSO url const permissionsToCheck = { origins: [Platform.SSO_URL + "/*"], }; const result = await chrome.permissions.contains(permissionsToCheck); this.sso_url_permitted = result; } } linux-entra-sso-1.11.0/src/policy.js000066400000000000000000000041451525176353400173050ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { getLogger, Deferred } from "./utils.js"; const log = getLogger("policy"); export class PolicyManager { static MANAGED_POLICIES_KEY = "wellKnownApps"; #apps = null; async load_policies() { const dfd = new Deferred(); chrome.storage.managed.get( PolicyManager.MANAGED_POLICIES_KEY, (data) => { if ( typeof data === "object" && data.hasOwnProperty("wellKnownApps") ) { this.#apps = { ...data.wellKnownApps }; log.debug("managed policies loaded"); } dfd.resolve(); }, ); return dfd.promise; } getPolicyUpdate(active_app_filters) { function matches_filter(app, policy) { return ( app.replace("*://", "https://") == "https://" + policy + "/*" ); } const catch_all = active_app_filters.find((value) => matches_filter(value, "*"), ); const gpo_update = { pending: false, filters_to_add: [], filters_to_remove: [], has_catch_all: catch_all !== undefined, apps_managed: this.#apps, }; if (this.#apps === null) return gpo_update; if (gpo_update.has_catch_all) { gpo_update.filters_to_remove.push(catch_all); gpo_update.pending = true; } for (const [app, enabled] of Object.entries(this.#apps)) { let filter = active_app_filters.find((value) => matches_filter(value, app), ); if (!enabled && filter !== undefined) { gpo_update.filters_to_remove.push(filter); gpo_update.pending = true; } else if (enabled && filter === undefined) { gpo_update.filters_to_add.push("https://" + app + "/*"); gpo_update.pending = true; } } return gpo_update; } } linux-entra-sso-1.11.0/src/state-machine.js000066400000000000000000000020231525176353400205210ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2026 Siemens */ import { getLogger } from "./utils.js"; /* * States are plain strings, the allowed transitions are declared as a * map of state -> successor states. Transitions outside that map are * rejected, so the model is the single source of truth. */ export class StateMachine { #log; #state; #transitions; constructor(name, transitions, initial) { this.#log = getLogger(name); this.#transitions = transitions; this.#state = initial; } get state() { return this.#state; } is_in(...states) { return states.includes(this.#state); } can_enter(next) { return this.#transitions[this.#state].includes(next); } /* Returns false if the transition is not allowed by the model. */ transition(next) { if (!this.can_enter(next)) return false; this.#log.info(`${this.#state} -> ${next}`); this.#state = next; return true; } } linux-entra-sso-1.11.0/src/utils.js000066400000000000000000000036071525176353400171500ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ const LOG_PREFIX = "[Linux Entra SSO]"; /* * Component scoped logger. The level maps to the matching console * function, so the browser console can filter by severity. */ class Logger { #tag; constructor(component) { this.#tag = `[${component}]`; } debug(...args) { console.debug(LOG_PREFIX, this.#tag, ...args); } info(...args) { console.info(LOG_PREFIX, this.#tag, ...args); } warn(...args) { console.warn(LOG_PREFIX, this.#tag, ...args); } error(...args) { console.error(LOG_PREFIX, this.#tag, ...args); } } export function getLogger(component) { return new Logger(component); } export async function load_icon(path, width) { const response = await fetch(chrome.runtime.getURL(path)); let imgBitmap = await createImageBitmap(await response.blob(), { resizeWidth: width, resizeHeight: width, }); const canvas = new OffscreenCanvas(width, width); const ctx = canvas.getContext("2d"); ctx.save(); ctx.drawImage(imgBitmap, 0, 0); ctx.restore(); return ctx.getImageData(0, 0, width, width); } export function jwt_get_payload(token) { const base64Url = token.split(".")[1]; const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); const jsonPayload = decodeURIComponent( atob(base64) .split("") .map(function (c) { return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); }) .join(""), ); return JSON.parse(jsonPayload); } /** * Promise that can externally be resolved or rejected. */ export class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.reject = reject; this.resolve = resolve; }); } } linux-entra-sso-1.11.0/tests/000077500000000000000000000000001525176353400160175ustar00rootroot00000000000000linux-entra-sso-1.11.0/tests/account_state_test.mjs000066400000000000000000000145011525176353400224260ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2026 Siemens * * Round-trip checks for the SSO login state across app restarts. * Run with: node tests/account_state_test.mjs */ const store = { local: {}, session: {} }; function area(name) { return { async get(key) { return key in store[name] ? { [key]: store[name][key] } : {}; }, async set(obj) { Object.assign(store[name], obj); }, }; } globalThis.chrome = { storage: { local: area("local"), session: area("session") }, }; const { AccountManager, Account } = await import("../src/account.js"); let failures = 0; function check(what, got, want) { const ok = JSON.stringify(got) === JSON.stringify(want); if (!ok) failures++; console.log(ok ? "ok " : "FAIL", what, "=", JSON.stringify(got)); } function reset(local = null, session = null) { store.local = local ? { ssostate: local } : {}; store.session = session ? { account_manager: session } : {}; } const acc = (username, active) => ({ broker_obj: { name: username, username }, active, avatar: null, }); /* 1. cold start: nothing persisted => SSO active, no accounts */ reset(); let am = new AccountManager(); await am.restore(); check("cold start isActive", am.isActive(), true); check("cold start hasAccounts", am.hasAccounts(), false); /* 2. session restore with a selected account => logged in */ reset(null, { broker_queried: true, accounts: [acc("a@x", true)] }); am = new AccountManager(); await am.restore(); check("session selected isActive", am.isActive(), true); check("session selected account", am.getActive()?.username(), "a@x"); /* 3. session restore, accounts but none selected => logged out */ reset(null, { broker_queried: true, accounts: [acc("a@x", false)] }); am = new AccountManager(); await am.restore(); check("session unselected isActive", am.isActive(), false); /* 4. local restore, state active => account restored */ reset({ state: true, accounts: [acc("a@x", true)] }); am = new AccountManager(); await am.restore(); check("local active isActive", am.isActive(), true); check("local active account", am.getActive()?.username(), "a@x"); /* 5. local restore, logged out => cached account data wiped from disk */ reset({ state: false, accounts: [acc("a@x", true)] }); am = new AccountManager(); await am.restore(); check("local logged-out isActive", am.isActive(), false); check("local logged-out hasAccounts", am.hasAccounts(), false); check("cached accounts wiped", store.local.ssostate.accounts, []); check("logged-out marker kept", store.local.ssostate.state, false); /* 6. round trip: logged in -> persist -> restart (session cleared) */ reset(null, { broker_queried: true, accounts: [acc("a@x", true)] }); am = new AccountManager(); await am.restore(); await am.persist(); store.session = {}; am = new AccountManager(); await am.restore(); check("restart logged in", am.isActive(), true); check("restart account", am.getActive()?.username(), "a@x"); /* 7. round trip: explicit logout -> persist -> restart */ reset(null, { broker_queried: true, accounts: [acc("a@x", true)] }); am = new AccountManager(); await am.restore(); am.setActive(false); am.logout(); await am.persist(); check("logout persists no accounts", store.local.ssostate.accounts, []); check("logout persists state", store.local.ssostate.state, false); store.session = {}; am = new AccountManager(); await am.restore(); check("restart stays logged out", am.isActive(), false); check("restart no accounts", am.hasAccounts(), false); /* 8. logging back in after a logout is allowed */ am.setActive(true); check("re-login isActive", am.isActive(), true); /* 9. account data provenance */ reset(); am = new AccountManager(); await am.restore(); check("cold start not authoritative", am.hasBrokerData(), false); check("cold start not provisional", am.hasProvisionalData(), false); reset({ state: true, accounts: [acc("a@x", true)] }); am = new AccountManager(); await am.restore(); check("disk restore is provisional", am.hasProvisionalData(), true); check("disk restore not authoritative", am.hasBrokerData(), false); reset(null, { broker_queried: true, accounts: [acc("a@x", true)] }); am = new AccountManager(); await am.restore(); check("session confirmed is authoritative", am.hasBrokerData(), true); check("session confirmed not provisional", am.hasProvisionalData(), false); reset(null, { broker_queried: false, accounts: [acc("a@x", true)] }); am = new AccountManager(); await am.restore(); check("unconfirmed session is provisional", am.hasProvisionalData(), true); /* 10. provisional data is replaced once the broker answers */ reset({ state: true, accounts: [acc("stale@x", true)] }); am = new AccountManager(); await am.restore(); check("before broker: stale account", am.getActive()?.username(), "stale@x"); const broker = { async getAccounts() { return [Account.fromSerial(acc("fresh@x", false))]; }, }; am.loadProfilePicture = async () => {}; await am.loadAccounts(broker); check("after broker: fresh account", am.getActive()?.username(), "fresh@x"); check("after broker: authoritative", am.hasBrokerData(), true); check("after broker: not provisional", am.hasProvisionalData(), false); /* authoritative data is not re-queried */ let queried = false; await am.loadAccounts({ async getAccounts() { queried = true; return []; }, }); check("authoritative data not re-queried", queried, false); /* 11. an empty broker response is authoritative and drops stale data */ reset({ state: true, accounts: [acc("stale@x", true)] }); am = new AccountManager(); await am.restore(); check("stale data is provisional", am.hasProvisionalData(), true); await am.loadAccounts({ async getAccounts() { return []; }, }); check("empty response is authoritative", am.hasBrokerData(), true); check("empty response drops stale data", am.hasAccounts(), false); check("empty response not provisional", am.hasProvisionalData(), false); /* 12. a missing response leaves the provenance undetermined */ reset({ state: true, accounts: [acc("stale@x", true)] }); am = new AccountManager(); await am.restore(); await am.loadAccounts({ async getAccounts() { return null; }, }); check("no response is not authoritative", am.hasBrokerData(), false); console.log(failures === 0 ? "\nALL PASS" : `\n${failures} FAILURE(S)`); process.exit(failures === 0 ? 0 : 1); linux-entra-sso-1.11.0/tests/linux_entra_sso_mock.py000066400000000000000000000101701525176353400226150ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MPL-2.0 # SPDX-FileCopyrightText: Copyright 2025 Siemens AG """ Mock implementation of the native part to test the web extension without having a broker. """ import importlib import sys import time import jwt les = importlib.import_module("linux-entra-sso") class SsoMibMock(les.SsoMib): """ Implementation of the SsoMib without broker communication. """ # random but stable MOCK_TENANT = "f52f0148-c8bb-4ee1-899b-8f93b0e4d63d" def __init__(self, daemon=False): # pylint: disable=unused-argument self.broker = True self._state_changed_cb = None def check_broker_online(self): """ The mock always simulates an available broker. """ return True def on_broker_state_changed(self, callback): """ Register a callback to be called when the broker state changes. """ self._state_changed_cb = callback def get_accounts(self): """ Returns two fake accounts with otherwise valid data. """ return { "accounts": [ { "name": "Account, Test (My Org Code)", "givenName": "Account, Test (My Org Code)", "username": "test.account@my-org.example.com", "homeAccountId": f"{self.MOCK_TENANT}-a975168d-a362-458b-af1c-a8982b1e8aac", "localAccountId": "a975168d-a362-458b-af1c-a8982b1e8aac", "clientInfo": jwt.encode( {"some": "payload"}, "secret", algorithm="HS256" ).split(".", maxsplit=1)[0], "realm": self.MOCK_TENANT, }, { "name": "Account, Admin (My Org Code)", "givenName": "Account, Admin (My Org Code)", "username": "test.admin@my-org.example.com", "homeAccountId": f"{self.MOCK_TENANT}-2f205376-88f7-47a4-be93-8aa7cae8e4fa", "localAccountId": "2f205376-88f7-47a4-be93-8aa7cae8e4fa", "clientInfo": jwt.encode( {"some": "payload"}, "secret", algorithm="HS256" ).split(".", maxsplit=1)[0], "realm": self.MOCK_TENANT, }, ] } def acquire_prt_sso_cookie( self, account, sso_url, scopes=les.SsoMib.GRAPH_SCOPES ): # pylint: disable=dangerous-default-value,unused-argument """ Return a fake PRT SSO Cookie. The returned data cannot be used to perform SSO. """ return { "account": account, "cookieContent": jwt.encode( {"scopes": " ".join(scopes)}, "secret", algorithm="HS256" ), "cookieName": "x-ms-RefreshTokenCredential", } def acquire_token_silently( self, account, scopes=les.SsoMib.GRAPH_SCOPES ): # pylint: disable=dangerous-default-value """ Return a fake (invalid) token. """ return { "brokerTokenResponse": { "accessToken": jwt.encode( {"scopes": " ".join(scopes)}, "secret", algorithm="HS256" ), "accessTokenType": 0, "idToken": jwt.encode( {"scopes": " ".join(scopes)}, "secret", algorithm="HS256" ), "account": account, "clientInfo": account["clientInfo"], "expiresOn": int(time.time() + 3600) * 1000, "extendedExpiresOn": int(time.time() + 2 * 3600) * 1000, "grantedScopes": scopes + ["profile"], } } def get_broker_version(self): """ Return the broker and script version (marked as mock). """ return { "linuxBrokerVersion": "2.0.1-mock", "native": f"{les.LINUX_ENTRA_SSO_VERSION}-mock", } les.SsoMib = SsoMibMock if __name__ == "__main__": if "--interactive" in sys.argv or "-i" in sys.argv: les.run_interactive() else: les.run_as_native_messaging()