pax_global_header00006660000000000000000000000064151450266110014513gustar00rootroot0000000000000052 comment=70283fa4106088887d2cfa06bd0786e665aaa6fd linux-entra-sso-1.8.0/000077500000000000000000000000001514502661100145715ustar00rootroot00000000000000linux-entra-sso-1.8.0/.github/000077500000000000000000000000001514502661100161315ustar00rootroot00000000000000linux-entra-sso-1.8.0/.github/workflows/000077500000000000000000000000001514502661100201665ustar00rootroot00000000000000linux-entra-sso-1.8.0/.github/workflows/build.yml000066400000000000000000000050021514502661100220050ustar00rootroot00000000000000# SPDX-FileCopyrightText: Copyright 2024 Siemens AG # # SPDX-License-Identifier: MPL-2.0 name: build browser extension on: push: branches: - main pull_request: workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to checkout the repository permissions: contents: read env: WEB_EXT_VERS: 8.9.0 jobs: reuse-and-codestyle: runs-on: ubuntu-24.04 steps: - name: checkout repository uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - 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 prettier uses: creyD/prettier_action@v4.6 with: dry: true prettier_options: '--check **/*.{js,html,css}' build-xpi: runs-on: ubuntu-24.04 permissions: id-token: write attestations: write steps: - name: checkout repository uses: actions/checkout@v4 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 - name: "web-ext lint Thunderbird" run: | npx web-ext@${{ env.WEB_EXT_VERS }} lint --source-dir build/thunderbird --self-hosted - name: upload Firefox and Thunderbird extension uses: actions/upload-artifact@v4 with: name: mozilla-xpi path: | build/**/*.xpi - name: upload chrome extension zip uses: actions/upload-artifact@v4 with: name: chrome-zip path: | build/chrome/ - name: upload debian package uses: actions/upload-artifact@v4 with: name: debian-package path: | pkgs/linux-entra-sso_*.deb - name: attest extension artifacts uses: actions/attest-build-provenance@v1 if: github.event_name == 'push' with: subject-path: | build/Linux-Entra-SSO-v* pkgs/linux-entra-sso_*.deb linux-entra-sso-1.8.0/.github/workflows/deploy-update-manifest.yml000066400000000000000000000014711514502661100252740ustar00rootroot00000000000000# 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@v4 - name: Setup Pages uses: actions/configure-pages@v5 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: '.pages/' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 linux-entra-sso-1.8.0/.github/workflows/release.yml000066400000000000000000000122051514502661100223310ustar00rootroot00000000000000# SPDX-FileCopyrightText: Copyright 2024 Siemens AG # # SPDX-License-Identifier: MPL-2.0 name: release browser extension on: push: tags: - 'v*.*' - 'v*.*.*' permissions: {} env: WEB_EXT_VERS: 8.9.0 jobs: release-extension: permissions: contents: write pull-requests: write id-token: write attestations: write runs-on: ubuntu-24.04 steps: - name: checkout repository uses: actions/checkout@v4 - 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 unlisted \ --approval-timeout 900000 \ --api-key ${{ secrets.AMO_API_KEY }} \ --api-secret ${{ secrets.AMO_API_SECRET }} \ --source-dir build/firefox \ --artifacts-dir build # 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@v4 with: name: firefox-signed-xpi path: | build/linux_entra_sso-*.xpi - name: upload debian package uses: actions/upload-artifact@v4 with: name: debian-package path: | pkgs/linux-entra-sso_*.deb - name: attest Firefox extension build uses: actions/attest-build-provenance@v1 with: subject-path: | build/linux_entra_sso-*.xpi pkgs/linux-entra-sso_*.deb - name: create release uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v2.0.6 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: create update manifest for Firefox run: | VERSION=$(echo ${{ github.ref_name }} | cut -c 2-) DIGEST="sha256:$(sha256sum build/linux_entra_sso-${VERSION}.xpi | cut -d ' ' -f 1)" LINK="https://github.com/siemens/linux-entra-sso/releases/download/v${VERSION}/linux_entra_sso-${VERSION}.xpi" jq --arg version "${VERSION}" --arg digest "${DIGEST}" --arg link "${LINK}" \ '."addons"."linux-entra-sso@example.com"."updates" += [{"version":$version, "update_link":$link, "update_hash":$digest}]' \ .pages/firefox/updates.json \ > .pages/firefox/updates.json.tmp && mv .pages/firefox/updates.json.tmp .pages/firefox/updates.json - 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 Mozilla update manifests uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6 with: add-paths: | .pages/firefox/updates.json .pages/thunderbird/updates.json commit-message: "chore: release Mozilla update manifests" branch: ci/release-firefox-update-manifest base: main title: "chore: release Mozilla update manifests [bot]" assignees: fmoessbauer reviewers: jan-kiszka author: ${{ env.GIT_COMMITTER }} committer: ${{ env.GIT_COMMITTER }} signoff: true body: | Publish update manifest for Mozilla extensions, version ${{ github.ref_name }}. - name: release Chrome extension on CWS uses: mnao305/chrome-extension-upload@4008e29e13c144d0f6725462cbd49b7c291b4928 # v5.0.0 with: file-path: build/Linux-Entra-SSO-*chrome.zip glob: true extension-id: jlnfnnolkbjieggibinobhkjdfbpcohn client-id: ${{ secrets.CWS_CLIENT_ID }} client-secret: ${{ secrets.CWS_CLIENT_SECRET }} refresh-token: ${{ secrets.CWS_REFRESH_TOKEN }} publish: false linux-entra-sso-1.8.0/.gitignore000066400000000000000000000002241514502661100165570ustar00rootroot00000000000000# 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.8.0/.pages/000077500000000000000000000000001514502661100157465ustar00rootroot00000000000000linux-entra-sso-1.8.0/.pages/firefox/000077500000000000000000000000001514502661100174105ustar00rootroot00000000000000linux-entra-sso-1.8.0/.pages/firefox/updates.json000066400000000000000000000105031514502661100217470ustar00rootroot00000000000000{ "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" } ] } } } linux-entra-sso-1.8.0/.pages/firefox/updates.json.license000066400000000000000000000001241514502661100233660ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/.pages/thunderbird/000077500000000000000000000000001514502661100202605ustar00rootroot00000000000000linux-entra-sso-1.8.0/.pages/thunderbird/updates.json000066400000000000000000000033731514502661100226260ustar00rootroot00000000000000{ "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" } ] } } } linux-entra-sso-1.8.0/.pages/thunderbird/updates.json.license000066400000000000000000000001211514502661100242330ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2025 Siemens SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/.prettierrc000066400000000000000000000000261514502661100167530ustar00rootroot00000000000000{ "tabWidth": 4 } linux-entra-sso-1.8.0/.prettierrc.license000066400000000000000000000001341514502661100203740ustar00rootroot00000000000000# SPDX-License-Identifier: MPL-2.0 # SPDX-FileCopyrightText: Copyright (c) Siemens AG, 2024 linux-entra-sso-1.8.0/CONTRIBUTING.md000066400000000000000000000066111514502661100170260ustar00rootroot00000000000000 # 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) 4. add release-notes to public release 5. manually inspect signed xpi (double check) 6. merge auto-created MR to enroll Firefox update manifest 7. publish CWS upload (answer questions on permission changes) 8. wait for CWS to review and sign extension, upload `.crx` to releases page linux-entra-sso-1.8.0/LICENSE000066400000000000000000000405271514502661100156060ustar00rootroot00000000000000Mozilla 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.8.0/LICENSES/000077500000000000000000000000001514502661100157765ustar00rootroot00000000000000linux-entra-sso-1.8.0/LICENSES/MPL-2.0.txt000066400000000000000000000405271514502661100174740ustar00rootroot00000000000000Mozilla 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.8.0/MAINTAINERS.md000066400000000000000000000003371514502661100166700ustar00rootroot00000000000000 # Maintainers - Felix Moessbauer (fmoessbauer) - Jan Kiszka (jan-kiszka) linux-entra-sso-1.8.0/Makefile000066400000000000000000000301761514502661100162400ustar00rootroot00000000000000# # 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/background.js \ src/broker.js \ src/device.js \ src/platform.js \ src/policy.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 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) ${Q}sed -i $(UPDATE_VERSION_PY) $(firefox_lconfig_dir)/linux-entra-sso.py # 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) ${Q}sed -i $(UPDATE_VERSION_PY) $(BROWSER_CONFIG_PATH)/linux-entra-sso.py 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.py install -m 0755 tests/linux_entra_sso_mock.py $(chrome_lconfig_dir)/linux_entra_sso.py install -m 0755 tests/linux_entra_sso_mock.py $(chromium_lconfig_dir)/linux_entra_sso.py install -m 0755 tests/linux_entra_sso_mock.py $(firefox_lconfig_dir)/linux_entra_sso.py install -m 0755 tests/linux_entra_sso_mock.py $(vivaldi_lconfig_dir)/linux_entra_sso.py #################################### # 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 ${Q}sed -i $(UPDATE_VERSION_PY) $(DESTDIR)/$(libexecdir)/linux-entra-sso/linux-entra-sso.py ${Q}sed -i $(UPDATE_PYTHON_INTERPRETER) $(DESTDIR)/$(libexecdir)/linux-entra-sso/linux-entra-sso.py # 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.py local-uninstall-chrome: rm -f $(chrome_lconfig_dir)/NativeMessagingHosts/linux_entra_sso.json rm -f $(chrome_lconfig_dir)/linux-entra-sso.py local-uninstall-chromium: rm -f $(chromium_lconfig_dir)/NativeMessagingHosts/linux_entra_sso.json rm -f $(chromium_lconfig_dir)/linux-entra-sso.py local-uninstall-firefox: rm -f $(firefox_lconfig_dir)/native-messaging-hosts/linux_entra_sso.json rm -f $(firefox_lconfig_dir)/linux-entra-sso.py local-uninstall-vivaldi: rm -f $(vivaldi_lconfig_dir)/NativeMessagingHosts/linux_entra_sso.json rm -f $(vivaldi_lconfig_dir)/linux-entra-sso.py local-uninstall: local-uninstall-brave local-uninstall-chrome local-uninstall-chromium local-uninstall-firefox local-uninstall-vivaldi .PHONY: clean release deb deb_clean .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.8.0/PRIVACY.md000066400000000000000000000043751514502661100162410ustar00rootroot00000000000000 # 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.8.0/README.md000066400000000000000000000155361514502661100160620ustar00rootroot00000000000000 # 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. ### Expired Tokens on Chrome Due to not having the `WebRequestsBlocking` API on Chrome, the extension needs to use a different mechanism to inject the token. While in Firefox the token is requested on-demand when hitting the SSO login URL, in Chrome the token is requested periodically. Then, a `declarativeNetRequest` API rule is setup to inject the token. As the lifetime of the tokens is limited and cannot be checked, outdated tokens might be injected. Further, a generic SSO URL must be used when requesting the token, instead of the actual one. ## 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.8.0/docs/000077500000000000000000000000001514502661100155215ustar00rootroot00000000000000linux-entra-sso-1.8.0/docs/global_install.md000066400000000000000000000065741514502661100210450ustar00rootroot00000000000000 # 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.8.0/icons/000077500000000000000000000000001514502661100157045ustar00rootroot00000000000000linux-entra-sso-1.8.0/icons/linux-entra-sso.svg000066400000000000000000000047511514502661100215040ustar00rootroot00000000000000 linux-entra-sso-1.8.0/icons/linux-entra-sso_128.png000066400000000000000000000113041514502661100220530ustar00rootroot00000000000000PNG  IHDR>a pHYstEXtSoftwarewww.inkscape.org<QIDATx{tU՝?s@^DX-Bx(VlHՙiY jW[Y3l1bX;uTKUS Ey#!!{~GȒss{>kݵt={-Cb$NB]kP3! >GIE.@$f߭Hr#ebODT\oG$ @ncܯH D;;$MS-ײ23y'Jmo-Att| e>[_UKq[@fT 4Z2 2ALs,A6DL}!-?Tx h{.(bD)I)ws'牲M=6|"t?'ڐHH:Ȕw(CNlHvFNe ii+qVhmE/6@C=;<}Z[C~g1]7\ȉL{A~ &=cP!돑aNmE~y3|(OU đr¼\W bHmӧ0TaV/$Tg]7o2}q:i% OktYYs  @/N*ȝ]>lp -Q =z:ІzO?<.LA_U*`Dp)T>tm ]PȌ, q#z掿a8"@vR0V]N %dGW> Hn~, ypBkKl RA%uͯ7ƪ@J}9a|%]z|oDƣ hI/hg`Ý[Iטqnt K48\<>i7-L{aA>r7c8Z.o_1gz0ZƸ @,)|-1]7"Fܹ_ծw9<p{e[Ж&8w=w=w-<4N]7wEMroWlea0q'ژ'"ґ,ZiӇYs&mtKEi(ggɦ;bCGZy˕5ZK i"Xr{4H .O}% ׏m@N7[^۪V/o#E-GEi#F37'&tz4=u+ԯ S*¼f.@?](*zYqZԹRh>wm`0ML~1m SQ|鑖}O`<֯GW' Ks795]3Ƚ]"ogx=䌼9ނrbvR2b=UJ ?N]p'!pe½2R{'P>1j3#{pNCu[t`y爐v#Bp {U.ײ0}_}r@JorU=4Ef,zeۇ9-@ y+ω{A:%/zIuR8j(#M g?X eʒBKHy!tU=zbF<יM\"K@h06i`.okr݀Ocwt(ݚ.A~NzH/.YY8 o~ѥAoH v}]AßuhU гO^ ;}:2vWY_e `yXqC$濻utnb֟*gƋ ߰m>s^9k嘨դ|}ޱnGK6oc'?Dyʹq"Qo?b][G%?o-!(\t'ːnK+O  vq]E7'{1|jFof^븰e|>;\nbGgt";@x1r/0[P+^{5 |C!++:in[mՔhΪ>-ŹbiiEkd/HآLtՃcCxa 0<{Z_2v7'O!,ǼG.wtq(SO8ĉ՛pTOr|nŀ宂 [~Jqbٛ;){j=c5y { -x.,y{OCL*^xEϾQygĭB7،/X?;sRy+9_m+]x_saoIu[xk~^Y{tQ}ƼpR 2ba1E΁Z70SG2a䆹z GaѢEٳcOP<]χaC׃zcsE@)}jqɂ!}nHZUxCii)ǎ}/1 6pU8f0cM46©zg9~*5ê2;"O(R$[? ]@nXF*={0yd?WTTƍCd7Ґ1*/ >Bڪo_QQ=zԯ,ULtO_ؿx"wy''Nnu"/OAnEUUUs=wtشi/++c֬Yq?8=m͛7SU,"2LD^"FD:LN-(++'7 > D760 L6Ha\ !PRRB»v믷tKwmI' VZѣ~2o] .$33aҤI\2 8<$GfՑaP^^΂ hjj"==sQ wG$̌8ۍ۝̄t]C|q8Hq8RG)#@ q8Hq8RG)#yQDNHuXDDFDVZjM\(œ8[[/wFU|5_l Q=Ht`x<Gӧ3gΌ"R9? 2Ε0`RDAQXr%#GDD馛Xf 6f vbF=OśpO}boo~0;d*?n$ j::&|0o~=0KUc@Dhܥqq ^-A0ꦈ GQ" -A\vDZ82ZA=pGvĀDG1#$@LLՑb%Gфd|H jCsslw!1-$L.E"v!N8Hq8Rx 0iH&.΢ Hq8R!1s*?IENDB`linux-entra-sso-1.8.0/icons/linux-entra-sso_128.png.license000066400000000000000000000001241514502661100234720ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/icons/linux-entra-sso_48.png000066400000000000000000000035301514502661100217760ustar00rootroot00000000000000PNG  IHDR00W pHYs tEXtSoftwarewww.inkscape.org<IDAThL_'uVtEdN".ٖtu˖Yt[5 s3[\jغ4M4%vC6 dNJ;ai bqqAdkzlRGcw-MZ|Ⱦ}瓾+"?>Z/PR^S;%#RPV,Ycv;ƒȢhlbw341D@+$ LXcY:MX7GenT0E P ˀ v;/` 9VG;V}KRV3P ֲb8l9yH! 1+>ۑLeֈ<~8v0^4= G4HBbDF=&zۅ^oż~֭*U+\Ȁmۑ,@LN.OM|ikjZ[-j@6I(mc|6mgni<Ҭjޱcm̡BIX,ay U[5f gY %d&;3.vz[QxZ0v X$_"k92b7,{!me٢gTxǷz}Sh -)1@<@e 9-ĤE V S4f@ RGEGM\@Rɕ);eyNaa|,(u$Lbt,6 gZ0:%eb_i=:EUG0Llv.!1zi`>0 KT'˗&,Yi{g|kdDIrz?Μg*j~洁 9qCC 8[ʫo\@W3uV\8z=`^8z*pjXoa{t/ x7 W F]⓼v/Icc~c Yorϔ$w]院KaSDs R5Ͽ|ir1ͥRVSsM͝~S:"Oݷur?{ʶQ9>EWlBWWAM 70YiEQL hE1)8 .C];ăZeZ^AP,D?T:0e3;fH:<ـ hgg(NSm6jaa8Ch`u(Z&oFp[3rfTUUaTpB.5k؈$5u!>>J'dƤϐ@ffpaÆHÆMDЇ ,4NP"rXDb&g"#}ƍCj/"q Ok)xs`0J 83g+|gTy:㻱 :Uw;2悪A `6?lawg5U2,\-H3B5oD@%?{|X ;aIENDB`linux-entra-sso-1.8.0/icons/linux-entra-sso_48.png.license000066400000000000000000000001241514502661100234130ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/icons/profile-outline.svg000066400000000000000000000015701514502661100215450ustar00rootroot00000000000000 linux-entra-sso-1.8.0/icons/profile-outline_48.png000066400000000000000000000012511514502661100220410ustar00rootroot00000000000000PNG  IHDRw= pHYsodtEXtSoftwarewww.inkscape.org<6IDATH?hq?]2\h:%JE,8ة:(4%*ةPũZݔ#\ I.}w0J葪0Ii9 ,,---'ժC?}#+Ƙt:jm;l6iQeYN#,랈0,A_`xU(s p59ZgE q8G?AZwaK'x!"ﺹ#Ud+V6IENDB`linux-entra-sso-1.8.0/icons/profile-outline_48.png.license000066400000000000000000000001241514502661100234600ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2021 YANDEX LLC SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/linux-entra-sso.py000077500000000000000000000312311514502661100202160ustar00rootroot00000000000000#!/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 ctypes import json import struct import sys import uuid from enum import Enum from signal import SIGINT 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 # prctl constants PR_SET_PDEATHSIG = 1 # 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 self._last_state_reported = False if daemon: self._introspect_broker() self._monitor_bus() def _introspect_broker(self): introspection = ET.fromstring(BROKER_DBUS_SPEC) self.broker = CompositeInterface(introspection)( self._bus, self.BROKER_NAME, self.BROKER_PATH ) self._report_state_change() 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] if new_owner: self._introspect_broker() else: # we need to ensure that the next dbus call will # wait until the broker is fully initialized again self.broker = None self._report_state_change() def _report_state_change(self): current_state = bool(self.broker) if self._state_changed_cb and self._last_state_reported != current_state: self._state_changed_cb(current_state) self._last_state_reported = current_state 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 initial state notify_state_change(bool(ssomib.broker)) loop = GLib.MainLoop() loop.run() def register_terminate_with_parent(): libc = ctypes.CDLL("libc.so.6") libc.prctl(PR_SET_PDEATHSIG, SIGINT, 0, 0, 0) print("Running as native messaging instance.", file=sys.stderr) print("For interactive mode, start with --interactive", file=sys.stderr) # on chrome and chromium, the parent process does not reliably # terminate the process when the parent process is killed. register_terminate_with_parent() ssomib = SsoMib(daemon=True) ssomib.on_broker_state_changed(notify_state_change) monitor = Thread(target=run_dbus_monitor) 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.8.0/platform/000077500000000000000000000000001514502661100164155ustar00rootroot00000000000000linux-entra-sso-1.8.0/platform/chrome/000077500000000000000000000000001514502661100176725ustar00rootroot00000000000000linux-entra-sso-1.8.0/platform/chrome/extension.json000066400000000000000000000001211514502661100225730ustar00rootroot00000000000000{ "external_update_url": "https://clients2.google.com/service/update2/crx" } linux-entra-sso-1.8.0/platform/chrome/extension.json.license000066400000000000000000000001241514502661100242170ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/platform/chrome/get-ext-id.py000077500000000000000000000007611514502661100222220ustar00rootroot00000000000000#!/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.8.0/platform/chrome/js/000077500000000000000000000000001514502661100203065ustar00rootroot00000000000000linux-entra-sso-1.8.0/platform/chrome/js/platform-chrome.js000066400000000000000000000060761514502661100237540ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { Platform } from "./platform.js"; import { ssoLog } from "./utils.js"; export class PlatformChrome extends Platform { browser = "Chrome"; #update_net_rules_cb = null; static CHROME_PRT_SSO_REFRESH_INTERVAL_MIN = 30; constructor() { super(); } update_request_handlers(enabled, account, broker) { super.update_request_handlers(enabled, account, broker); this.#update_net_rules_cb = this.#update_net_rules.bind(this, broker); if (!enabled) { chrome.alarms.onAlarm.removeListener(this.#update_net_rules_cb); chrome.alarms.clear("prt-sso-refresh"); this.#clear_net_rules(); return; } this.#ensure_refresh_alarm("prt-sso-refresh"); this.#update_net_rules(broker); } /* * Ensure the alarm is armed exactly once. */ async #ensure_refresh_alarm(alarm_id) { const alarm = await chrome.alarms.get(alarm_id); if (!alarm) { await chrome.alarms.create(alarm_id, { periodInMinutes: PlatformChrome.CHROME_PRT_SSO_REFRESH_INTERVAL_MIN, }); } if (!chrome.alarms.onAlarm.hasListener(this.#update_net_rules_cb)) { chrome.alarms.onAlarm.addListener(this.#update_net_rules_cb); } } async #clear_net_rules() { ssoLog("clear network rules"); const oldRules = await chrome.declarativeNetRequest.getSessionRules(); const oldRuleIds = oldRules.map((rule) => rule.id); await chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: oldRuleIds, }); } async #update_net_rules(broker, e) { ssoLog("update network rules"); let prt = undefined; try { prt = await broker.acquirePrtSsoCookie( this.account, Platform.SSO_URL, ); } catch (error) { ssoLog(error); return; } const newRules = [ { id: 1, priority: 1, condition: { urlFilter: Platform.SSO_URL + "/*", 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, }); ssoLog("network rules updated"); } } linux-entra-sso-1.8.0/platform/chrome/js/platform-factory.js000066400000000000000000000003351514502661100241360ustar00rootroot00000000000000/* * 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.8.0/platform/chrome/linux_entra_sso.json000066400000000000000000000005061514502661100240020ustar00rootroot00000000000000{ "name": "linux_entra_sso", "description": "Entra ID SSO via Microsoft Identity Broker", "path": "/usr/local/lib/linux-entra-sso/linux-entra-sso.py", "type": "stdio", "allowed_origins": [ "chrome-extension://{extension_id}/", "chrome-extension://jlnfnnolkbjieggibinobhkjdfbpcohn/" ] } linux-entra-sso-1.8.0/platform/chrome/linux_entra_sso.json.license000066400000000000000000000001241514502661100254170ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/platform/chrome/manifest.json000066400000000000000000000017571514502661100224050ustar00rootroot00000000000000{ "description": "Entra ID SSO via Microsoft Identity Broker", "manifest_version": 3, "name": "Linux Entra SSO", "version": "1.8.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.8.0/platform/chrome/manifest.json.license000066400000000000000000000001241514502661100240110ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/platform/chrome/storage-schema.json000066400000000000000000000003561514502661100234730ustar00rootroot00000000000000{ "type": "object", "properties": { "wellKnownApps": { "description": "Apps that are allowed to perform background SSO.", "type": "object", "additionalProperties": { "type": "boolean" } } } } linux-entra-sso-1.8.0/platform/chrome/storage-schema.json.license000066400000000000000000000001241514502661100251050ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2025 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/platform/firefox/000077500000000000000000000000001514502661100200575ustar00rootroot00000000000000linux-entra-sso-1.8.0/platform/firefox/js/000077500000000000000000000000001514502661100204735ustar00rootroot00000000000000linux-entra-sso-1.8.0/platform/firefox/js/platform-factory.js000066400000000000000000000003401514502661100243170ustar00rootroot00000000000000/* * 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.8.0/platform/firefox/js/platform-firefox.js000066400000000000000000000040621514502661100243170ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { Platform } from "./platform.js"; import { ssoLog } from "./utils.js"; export class PlatformFirefox extends Platform { browser = "Firefox"; /* PRT injection state */ #on_before_send_headers = null; constructor() { super(); } setIconDisabled() { chrome.action.setIcon({ path: "/icons/linux-entra-sso.svg", }); } update_request_handlers(enabled, account, broker) { super.update_request_handlers(enabled, account, broker); /* * We need to bind, as the handler is called from a different context. * To be able to deregister the handler, we need to assign it to a * named symbol. */ this.#on_before_send_headers = this.#onBeforeSendHeaders.bind( this, broker, ); chrome.webRequest.onBeforeSendHeaders.removeListener( this.#on_before_send_headers, ); if (!enabled || this.well_known_app_filters.length == 0) return; chrome.webRequest.onBeforeSendHeaders.addListener( this.#on_before_send_headers, { urls: this.well_known_app_filters, types: ["main_frame", "sub_frame"], }, ["blocking", "requestHeaders"], ); } async #onBeforeSendHeaders(broker, e) { // filter out requests that are not part of the OAuth2.0 flow if (!e.url.startsWith(Platform.SSO_URL)) { return { requestHeaders: e.requestHeaders }; } try { let prt = await broker.acquirePrtSsoCookie(this.account, e.url); // ms-oapxbc OAuth2 protocol extension ssoLog("inject PRT SSO into request headers"); e.requestHeaders.push({ name: prt.cookieName, value: prt.cookieContent, }); } catch (error) { ssoLog(error); } return { requestHeaders: e.requestHeaders }; } } linux-entra-sso-1.8.0/platform/firefox/linux_entra_sso.json000066400000000000000000000004411514502661100241650ustar00rootroot00000000000000{ "name": "linux_entra_sso", "description": "Entra ID SSO via Microsoft Identity Broker", "path": "/usr/local/lib/linux-entra-sso/linux-entra-sso.py", "type": "stdio", "allowed_extensions": [ "linux-entra-sso@example.com", "@linux-entra-sso.tb" ] } linux-entra-sso-1.8.0/platform/firefox/linux_entra_sso.json.license000066400000000000000000000001241514502661100256040ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/platform/firefox/manifest.json000066400000000000000000000020331514502661100225560ustar00rootroot00000000000000{ "description": "Entra ID SSO via Microsoft Identity Broker", "manifest_version": 3, "name": "Linux Entra SSO", "version": "1.8.0", "icons": { "48": "icons/linux-entra-sso.svg" }, "browser_specific_settings": { "gecko": { "id": "linux-entra-sso@example.com", "strict_min_version": "128.0", "update_url": "https://siemens.github.io/linux-entra-sso/firefox/updates.json" } }, "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.8.0/platform/firefox/manifest.json.license000066400000000000000000000001241514502661100241760ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2024 Siemens AG SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/platform/thunderbird/000077500000000000000000000000001514502661100207275ustar00rootroot00000000000000linux-entra-sso-1.8.0/platform/thunderbird/js/000077500000000000000000000000001514502661100213435ustar00rootroot00000000000000linux-entra-sso-1.8.0/platform/thunderbird/js/platform-factory.js000066400000000000000000000003541514502661100251740ustar00rootroot00000000000000/* * 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.8.0/platform/thunderbird/js/platform-thunderbird.js000066400000000000000000000005071514502661100260370ustar00rootroot00000000000000/* * 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.8.0/platform/thunderbird/manifest.json000066400000000000000000000021211514502661100234240ustar00rootroot00000000000000{ "description": "Entra ID SSO via Microsoft Identity Broker", "manifest_version": 3, "name": "Linux Entra SSO", "version": "1.8.0", "icons": { "48": "icons/linux-entra-sso.svg" }, "browser_specific_settings": { "gecko": { "id": "@linux-entra-sso.tb", "strict_min_version": "128.0", "update_url": "https://siemens.github.io/linux-entra-sso/thunderbird/updates.json" } }, "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.8.0/platform/thunderbird/manifest.json.license000066400000000000000000000001211514502661100250430ustar00rootroot00000000000000SPDX-FileCopyrightText: Copyright 2025 Siemens SPDX-License-Identifier: MPL-2.0 linux-entra-sso-1.8.0/popup/000077500000000000000000000000001514502661100157345ustar00rootroot00000000000000linux-entra-sso-1.8.0/popup/menu.css000066400000000000000000000107631514502661100174210ustar00rootroot00000000000000/* * 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; } /* 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); } :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); } 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 { 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); } 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.8.0/popup/menu.html000066400000000000000000000077151514502661100176000ustar00rootroot00000000000000
Avatar
Guest
linux-entra-sso-1.8.0/popup/menu.js000066400000000000000000000211721514502661100172410ustar00rootroot00000000000000/* * 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(); annotate_body_if("has-account", m.accounts.length); annotate_body_if("nm-connected", m.nm_connected); 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); annotate_by_id_if("broker-state", "connected", m.broker_online); document.getElementById("broker-state-value").innerText = m.broker_online ? "connected" : "disconnected"; document.getElementById("broker-version").innerText = m.broker_version; if (m.host_version) { let pvers = chrome.runtime.getManifest().version; let vstr = "v" + pvers; if (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); } 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://") || tab.url.startsWith(sso_url) ) { 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.8.0/src/000077500000000000000000000000001514502661100153605ustar00rootroot00000000000000linux-entra-sso-1.8.0/src/account.js000066400000000000000000000204511514502661100173540ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { ssoLog, load_icon, ssoLogError } from "./utils.js"; /* refresh the token if only x time is left */ const TOKEN_MIN_VALIDITY_MS = 60 * 1000; export class Account { #broker_obj = null; #avatar_imgdata = null; 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) { this.#avatar_imgdata = await load_icon( "/icons/profile-outline_48.png", 48, ); } return this.#avatar_imgdata; } setAvatarImgData(data) { this.#avatar_imgdata = data; } async getDecoratedAvatar(color, 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(); const img_margin = color === null ? 0 : lineWidth + 1; ctx.beginPath(); ctx.arc( width / 2, width / 2, width / 2 - img_margin, 0, Math.PI * 2, false, ); ctx.clip(); ctx.drawImage( buffer, 0, 0, sWidth, sWidth, img_margin, img_margin, width - img_margin * 2, width - img_margin * 2, ); ctx.restore(); if (color === null) { return ctx.getImageData(0, 0, width, width); } ctx.strokeStyle = color; ctx.lineWidth = lineWidth; ctx.beginPath(); ctx.arc( width / 2, width / 2, width / 2 - Math.min(1, lineWidth / 2), 0, Math.PI * 2, false, ); ctx.stroke(); return ctx.getImageData(0, 0, width, width); } toSerial() { return { broker_obj: this.brokerObject(), active: this.active }; } static fromSerial(serial) { let acc = new Account(serial.broker_obj); acc.active = serial.active; return acc; } } export class AccountManager { #registered = []; #queried = false; hasAccounts() { return this.#registered.length != 0; } /** * @returns if we got account data from the broker */ hasBrokerData() { return this.#queried; } getActive() { return this.#registered.find((a) => a.active); } 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) { ssoLog("no account found with username " + username); return undefined; } this.logout(); account.active = true; return account; } async loadAccounts(broker) { if (this.hasBrokerData()) return; ssoLog("loading accounts"); let _accounts = []; try { _accounts = await broker.getAccounts(); } catch (error) { ssoLog(error); } if (!_accounts || !_accounts.length) { this.#registered = []; return; } // if we already got an account from storage, select the // corresponding one from the broker as active. const last_username = this.getActive()?.username(); this.#registered = _accounts; if (last_username && this.selectAccount(last_username)) { ssoLog( "select previously used account: " + this.getActive().username(), ); } else { this.selectAccount(); ssoLog("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; } try { const graph_token = await broker.acquireTokenSilently(account); ssoLog("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) { ssoLog(error); 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.save(); ctx.beginPath(); ctx.arc(24, 24, 24, 0, Math.PI * 2, false); ctx.clip(); ctx.drawImage(avatar, 0, 0); ctx.restore(); /* 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); /* store image data */ ctx.clearRect(0, 0, 48, 48); ctx.drawImage(avatar, 0, 0, 48, 48); account.setAvatarImgData(ctx.getImageData(0, 0, 48, 48)); account.avatar = dataUrl; } else { ssoLog( "Warning: 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.getActive() != null, accounts: this.getActive() ? this.#registered.map((a) => a.toSerial()) : [], }; return chrome.storage.local.set({ ssostate }); } async restore() { const data = await chrome.storage.local.get("ssostate"); let active_acc = undefined; if (!data.ssostate) { ssoLog("no preserved state found"); // if the SSO is not explicitly disabled, we assume it is on. return true; } const state_active = data.ssostate.state; if (state_active && data.ssostate.accounts) { this.#registered = data.ssostate.accounts.map((a) => Account.fromSerial(a), ); if (!state_active) this.logout(); active_acc = this.getActive(); if (active_acc) { ssoLog( "temporarily using last-known account: " + active_acc.username(), ); } } return state_active; } } linux-entra-sso-1.8.0/src/background.js000066400000000000000000000142001514502661100200320ustar00rootroot00000000000000/* * 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 { ssoLog } from "./utils.js"; import { PolicyManager } from "./policy.js"; import { DeviceManager } from "./device.js"; const PLATFORM = create_platform(); let broker = null; let policyManager = null; let accountManager = null; let deviceManager = null; let initialized = false; let state_active = true; let port_menu = null; /* * Check if all conditions for SSO are met */ function is_operational() { return state_active && accountManager.getActive(); } async function on_permissions_changed() { ssoLog("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(); 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); let color = null; chrome.action.setTitle({ title: icon_title, }); if (!broker.isRunning()) { color = "#cc0000"; } for (const r of [16, 32, 48]) { imgdata[r] = await account.getDecoratedAvatar(color, r); } chrome.action.setIcon({ imageData: imgdata, }); chrome.action.setBadgeText({ text: action_needed ? "1" : null, }); return; } /* inactive states */ PLATFORM.setIconDisabled(); let title = "EntraID SSO disabled"; if (state_active) 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)"; chrome.action.setBadgeText({ text: "1", }); } // 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) { const gpo_update = policyManager.getPolicyUpdate( PLATFORM.well_known_app_filters, ); let action_needed = !PLATFORM.sso_url_permitted || gpo_update.pending; update_tray(action_needed); if (!ui_only && broker.isConnected()) { ssoLog("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) { notify_state_change(true); } }); port_menu.postMessage({ event: "stateChanged", accounts: accountManager.getRegistered().map((a) => a.toMenuObject()), broker_online: broker.isRunning(), nm_connected: broker.isConnected(), device: deviceManager.getDevice(), enabled: state_active, host_version: PLATFORM.host_versions.native, broker_version: PLATFORM.host_versions.broker, sso_url: PLATFORM.getSsoUrl(), gpo_update: gpo_update, }); } async function on_message_menu(request) { if (request.command == "enable") { state_active = true; const account = accountManager.selectAccount(request.username); if (account) ssoLog("select account " + account.username()); } else if (request.command == "disable") { state_active = false; accountManager.logout(); ssoLog("disable SSO"); } accountManager.persist(); notify_state_change(); } async function on_broker_state_change(online) { if (online) { ssoLog("connection to broker restored"); // only reload data if we did not see the broker before if (!accountManager.hasBrokerData()) { await accountManager.loadAccounts(broker); accountManager.persist(); await deviceManager.loadDeviceInfo(broker); notify_state_change(); } } else { ssoLog("lost connection to broker"); } notify_state_change(true); } async function on_storage_changed(_changes, areaName) { if (areaName == "managed") { await policyManager.load_policies(); } } function on_startup() { if (initialized) { ssoLog("linux-entra-sso already initialized"); return; } initialized = true; ssoLog("start linux-entra-sso on " + PLATFORM.browser); 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().then((active) => { state_active = active; }), ]).then(() => { broker.connect(); PLATFORM.setup(broker).then(() => { notify_state_change(true); }); notify_state_change(); }); chrome.runtime.onConnect.addListener((port) => { port_menu = port; port_menu.onMessage.addListener(on_message_menu); port_menu.onDisconnect.addListener(() => { port_menu = null; }); 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.8.0/src/broker.js000066400000000000000000000117411514502661100172060ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { ssoLog, ssoLogError, Deferred } from "./utils.js"; import { Account } from "./account.js"; /** * Queue to resolve promises, once the data arrives from the * remote backend. */ export class RpcHandlerQueue { #queue = []; register_handle(id) { const handle = { id: id, dfd: new Deferred(), }; this.#queue.push(handle); return handle.dfd.promise; } resolve_handle(id, data) { const idx = this.#queue.findIndex((hdl) => hdl.id == id); if (idx !== -1) { this.#queue[idx].dfd.resolve(data); this.#queue.splice(idx, 1); } } reject_handle(id, data) { const idx = this.#queue.findIndex((hdl) => hdl.id == id); if (idx !== -1) { this.#queue[idx].dfd.reject(data); this.#queue.splice(idx, 1); } } } export class Broker { #name = null; #notify_fn = null; #port_native = null; #rpc_queue = new RpcHandlerQueue(); #online = false; constructor(name, state_change_fn) { this.#name = name; this.#notify_fn = state_change_fn; } connect() { this.#port_native = chrome.runtime.connectNative(this.#name); this.#port_native.onDisconnect.addListener(() => { this.#port_native = null; if (chrome.runtime.lastError) { ssoLogError( "Error in native application connection: " + chrome.runtime.lastError.message, ); } else { ssoLogError("Native application connection closed."); } this.#notify_fn(false); }); this.#port_native.onMessage.addListener( this.#on_message_native.bind(this), ); ssoLog("Broker created"); } isConnected() { return this.#port_native !== null; } isRunning() { return this.#online; } getAccounts() { this.#port_native.postMessage({ command: "getAccounts" }); return this.#rpc_queue.register_handle("getAccounts"); } async acquireTokenSilently(account) { this.#port_native.postMessage({ command: "acquireTokenSilently", account: account.brokerObject(), }); return this.#rpc_queue.register_handle("acquireTokenSilently"); } async acquirePrtSsoCookie(account, ssoUrl) { this.#port_native.postMessage({ command: "acquirePrtSsoCookie", account: account.brokerObject(), ssoUrl: ssoUrl, }); return this.#rpc_queue.register_handle("acquirePrtSsoCookie"); } async getVersion() { this.#port_native.postMessage({ command: "getVersion" }); return this.#rpc_queue.register_handle("getVersion"); } #on_message_native(response) { /* handle events (not an RPC response) */ if (response.command == "brokerStateChanged") { if (response.message == "online") this.#online = true; else this.#online = false; this.#notify_fn(this.#online); return; } /* on rpc messages, reject all responses that have errors */ if ("error" in response.message) { this.#rpc_queue.reject_handle(response.command, { ...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", { ...response.message.brokerTokenResponse.error, }); } else { this.#rpc_queue.resolve_handle("acquireTokenSilently", { ...response.message.brokerTokenResponse, }); } } else { ssoLog("unknown command: " + response.command); } } } linux-entra-sso-1.8.0/src/device.js000066400000000000000000000050401514502661100171540ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { ssoLog, ssoLogError, jwt_get_payload } from "./utils.js"; export class Device { name = null; compliant = null; constructor(name, compliant) { this.name = name; this.compliant = compliant; } } export class DeviceManager { static DEVICE_REFRESH_INTERVAL_MIN_MS = 30 * 60 * 1000; #am = null; #last_refresh = 0; 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) { 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"]) { ssoLog("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) { ssoLogError("failed to query device state"); return false; } const data = await response.json(); this.#last_refresh = Date.now(); this.device = new Device(data.displayName, data.isCompliant); ssoLog("updated device information"); return true; } getDevice() { return this.device; } } linux-entra-sso-1.8.0/src/platform.js000066400000000000000000000037531514502661100175520ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { ssoLog } from "./utils.js"; 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; 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) { try { this.host_versions = await broker.getVersion(); } catch (error) { ssoLog(error); } } 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; } 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.8.0/src/policy.js000066400000000000000000000040751514502661100172230ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ import { ssoLog, Deferred } from "./utils.js"; 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 }; ssoLog("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.8.0/src/utils.js000066400000000000000000000026151514502661100170620ustar00rootroot00000000000000/* * SPDX-License-Identifier: MPL-2.0 * SPDX-FileCopyrightText: Copyright 2025 Siemens */ export function ssoLog(message) { console.log("[Linux Entra SSO]", message); } export function ssoLogError(message) { console.error("[Linux Entra SSO]", message); } 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.8.0/tests/000077500000000000000000000000001514502661100157335ustar00rootroot00000000000000linux-entra-sso-1.8.0/tests/linux_entra_sso_mock.py000066400000000000000000000076531514502661100225450ustar00rootroot00000000000000#!/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 def on_broker_state_changed(self, callback): # pylint: disable=unused-argument """ We do not implement state changes yet. """ 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()