├── .github └── workflows │ ├── .gitignore │ ├── act.sh │ ├── create-release.yml │ ├── on-release.yml │ ├── on-version-changed.yml │ ├── publish-release.yml │ └── pull-request.yml ├── .gitignore ├── .wp-env.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── create-release-artifacts.sh └── prepare-release.sh ├── composer.json ├── composer.lock ├── openid-connect-server.php ├── phpcs.xml ├── src ├── Http │ ├── Handlers │ │ ├── AuthenticateHandler.php │ │ ├── AuthorizeHandler.php │ │ ├── ConfigurationHandler.php │ │ ├── TokenHandler.php │ │ ├── UserInfoHandler.php │ │ └── WebKeySetsHandler.php │ ├── RequestHandler.php │ └── Router.php ├── OpenIDConnectServer.php ├── SiteStatusTests.php └── Storage │ ├── AuthorizationCodeStorage.php │ ├── ClientCredentialsStorage.php │ ├── ConsentStorage.php │ ├── PublicKeyStorage.php │ └── UserClaimsStorage.php ├── tests ├── .env ├── .gitignore ├── .yarnrc.yml ├── README.md ├── env.d.ts ├── index.ts ├── package.json ├── src │ ├── HttpsClient.ts │ ├── HttpsServer.ts │ └── OpenIdClient.ts ├── tsconfig.json └── yarn.lock └── uninstall.php /.github/workflows/.gitignore: -------------------------------------------------------------------------------- 1 | .secrets 2 | payload.json 3 | -------------------------------------------------------------------------------- /.github/workflows/act.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Act allows running GitHub actions locally: 4 | # https://github.com/nektos/act 5 | 6 | act --secret-file .github/workflows/.secrets -e .github/workflows/payload.json "$@" 7 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create a release 2 | run-name: Create release ${{ inputs.version }} 3 | 4 | on: 5 | # Trigger manually from https://github.com/Automattic/wp-openid-connect-server/actions/workflows/create-release.yml 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: "Release version, e.g. 1.2.3" 10 | required: true 11 | type: string 12 | 13 | workflow_call: 14 | inputs: 15 | version: 16 | description: "Release version, e.g. 1.2.3" 17 | required: true 18 | type: string 19 | 20 | jobs: 21 | create-release: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: write 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | - name: Create artifacts 30 | run: ./bin/create-release-artifacts.sh ${{ inputs.version }} 31 | 32 | - name: Create release 33 | uses: ncipollo/release-action@v1 34 | with: 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | tag: ${{ inputs.version }} 37 | commit: ${{ github.sha }} 38 | artifacts: "release/openid-connect-server-${{ inputs.version }}.tar.gz,release/openid-connect-server-${{ inputs.version }}.zip" 39 | artifactErrorsFailBuild: true 40 | draft: true 41 | -------------------------------------------------------------------------------- /.github/workflows/on-release.yml: -------------------------------------------------------------------------------- 1 | name: On new release, publish to plugin directory 2 | run-name: Publish ${{ github.event.release.tag_name }} to plugin directory 3 | 4 | on: 5 | # Trigger manually from https://github.com/Automattic/wp-openid-connect-server/actions/workflows/on-release-published.yml 6 | workflow_dispatch: 7 | 8 | release: 9 | types: [released] 10 | 11 | jobs: 12 | call-publish-release: 13 | uses: ./.github/workflows/publish-release.yml 14 | with: 15 | version: ${{ github.event.release.tag_name }} 16 | secrets: 17 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 18 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 19 | -------------------------------------------------------------------------------- /.github/workflows/on-version-changed.yml: -------------------------------------------------------------------------------- 1 | name: On changes to composer.json, create a release if version changed 2 | 3 | on: 4 | # Trigger manually from https://github.com/Automattic/wp-openid-connect-server/actions/workflows/on-version-changed.yml 5 | workflow_dispatch: 6 | 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - 'composer.json' 12 | 13 | jobs: 14 | on-version-changed: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | changed: ${{ steps.output.outputs.changed }} 18 | version: ${{ steps.output.outputs.version }} 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 2 24 | 25 | - uses: salsify/action-detect-and-tag-new-version@v2 26 | id: check-version 27 | with: 28 | version-command: jq '.version' composer.json 29 | create-tag: false 30 | 31 | - name: Set output 32 | if: steps.check-version.outputs.current-version != steps.check-version.outputs.previous-version 33 | id: output 34 | run: | 35 | echo "Detected version change from ${{ steps.check-version.outputs.previous-version }} to ${{ steps.check-version.outputs.current-version }}" 36 | echo ::set-output name=changed::true 37 | echo ::set-output name=version::${{ steps.check-version.outputs.current-version }} 38 | 39 | call-create-release: 40 | needs: on-version-changed 41 | if: needs.on-version-changed.outputs.changed == 'true' 42 | uses: ./.github/workflows/create-release.yml 43 | permissions: 44 | contents: write 45 | with: 46 | version: ${{ needs.on-version-changed.outputs.version }} 47 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release to plugin directory 2 | run-name: Publish ${{ inputs.version }} to plugin directory 3 | 4 | on: 5 | # Trigger manually from https://github.com/Automattic/wp-openid-connect-server/actions/workflows/publish-release.yml 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: "Version of the release to publish, e.g. 1.2.3" 10 | required: true 11 | type: string 12 | 13 | workflow_call: 14 | inputs: 15 | version: 16 | description: "Version of the release to publish, e.g. 1.2.3" 17 | required: true 18 | type: string 19 | secrets: 20 | SVN_USERNAME: 21 | required: true 22 | SVN_PASSWORD: 23 | required: true 24 | jobs: 25 | publish-release: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v2 30 | 31 | - name: Build release 32 | # We don't use the release artifacts here, but the script will also create the release directory, and that's 33 | # what will be uploaded to SVN. 34 | run: ./bin/create-release-artifacts.sh ${{ inputs.version }} 35 | 36 | - name: Install Subversion 37 | run: sudo apt-get install subversion -y 38 | 39 | - name: Deploy to plugin directory 40 | uses: 10up/action-wordpress-plugin-deploy@stable 41 | with: 42 | generate-zip: false 43 | env: 44 | SLUG: openid-connect-server 45 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 46 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 47 | BUILD_DIR: release/${{ inputs.version }} 48 | VERSION: ${{ inputs.version }} 49 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: PR checks 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize ] 6 | 7 | jobs: 8 | lint-plugin: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | 14 | - name: Install composer dependencies 15 | run: composer install 16 | 17 | - name: Run phpcs 18 | run: composer phpcs 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | readme.txt 3 | release 4 | /svn/ 5 | -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": "WordPress/WordPress#6.8", 3 | "phpVersion": "7.4", 4 | "plugins": [ "." ], 5 | "config": { 6 | "OIDC_PUBLIC_KEY": "<<-EOK\n-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwFc6WHsBl5e8LQMf3adq\nXGhklp/GbEgC4n+uex2bHo8Ly0Up36HrSrXtMSau6psOkEXdAGEUFZ8YjGdtx7iZ\n088nZ+p7jEsQ+xizYmqeWP/f+tV7aQJsRRAbiRfa/91gWrkQOGomJrHJVftKr+53\ndKMHDi6yRZQ6igEJTmBg2ClATBKLtyD9uHxHeCAMnvR2wWGYmMv1BLD36ySQPiFC\nqKXccUDUscuhYSSk/ODOonw0m00yJrlEe0cotrGP5Qyn9or2fDsjYt0TLD1apAgo\njQX+pJ4m0S8tzo+EMqJHM+eJl8g3QwQewcA2keLDInACtktBY7o3LOThkF3j17n5\nBeA8jEUDusqwt2kyGYakdSXFYufPgQsVChJouZxyY/iKf57kd1wfEcAvqRRHOB7l\nFIHbw18KeE4GRBNxawKDwPmXYSwe90uexuPNdaNEX5RDQw62/zwtbL0GBja8+QKU\nWqQJw12/kmfWGuKfRhEzisMO39FpX1yqDSzh5OpwlgOM3lbpQHur6Hrg3gBLYJYi\ngHy9UEq3IMsTtpMM0pgNK1bEBKu71zTqwIpDajFBAz8yUqQ8S9JXUihckUxXLaQe\n2RagfRQ91ubJJJhejfcWBA5EpwhsCU+RPsNw4rn1PkKJKXK27gYdKr5YZRtOQ5Ys\nx9N4z9IddG7tIzH1nmCh/ncCAwEAAQ==\n-----END PUBLIC KEY-----\nEOK", 7 | "OIDC_PRIVATE_KEY": "<<-EOK\n-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEAwFc6WHsBl5e8LQMf3adqXGhklp/GbEgC4n+uex2bHo8Ly0Up\n36HrSrXtMSau6psOkEXdAGEUFZ8YjGdtx7iZ088nZ+p7jEsQ+xizYmqeWP/f+tV7\naQJsRRAbiRfa/91gWrkQOGomJrHJVftKr+53dKMHDi6yRZQ6igEJTmBg2ClATBKL\ntyD9uHxHeCAMnvR2wWGYmMv1BLD36ySQPiFCqKXccUDUscuhYSSk/ODOonw0m00y\nJrlEe0cotrGP5Qyn9or2fDsjYt0TLD1apAgojQX+pJ4m0S8tzo+EMqJHM+eJl8g3\nQwQewcA2keLDInACtktBY7o3LOThkF3j17n5BeA8jEUDusqwt2kyGYakdSXFYufP\ngQsVChJouZxyY/iKf57kd1wfEcAvqRRHOB7lFIHbw18KeE4GRBNxawKDwPmXYSwe\n90uexuPNdaNEX5RDQw62/zwtbL0GBja8+QKUWqQJw12/kmfWGuKfRhEzisMO39Fp\nX1yqDSzh5OpwlgOM3lbpQHur6Hrg3gBLYJYigHy9UEq3IMsTtpMM0pgNK1bEBKu7\n1zTqwIpDajFBAz8yUqQ8S9JXUihckUxXLaQe2RagfRQ91ubJJJhejfcWBA5Epwhs\nCU+RPsNw4rn1PkKJKXK27gYdKr5YZRtOQ5Ysx9N4z9IddG7tIzH1nmCh/ncCAwEA\nAQKCAgEAntDnqluXCvcNOBWWYE12c2r3c6/mBU7IowFOrvXZObhfwq4PT0rBn+Ts\nP2NzluAFfHdTbpa6IMhHWtekO/9fdRddsF5bOZJaKRtSM3dZ3J8a8GEKD8EiaTxN\noDuEZzUB9KdOj2aGrfirCEYAe5hqJnLexoUkSnOEiqMkbgUCsjoNj0LY/pNNLOHh\nmXzNLwsoa7EMcU4nuIHvk8eTGtoX0m1xwkoH1e8QB8hcOzndJSltvrZVLMhZlXlC\nUuf2quhyYA6KTDYAmAd3Z1YZ28uowBktE/xKjPtp1V+Hhs+b/221nRCDIODsDIuM\nA5W8Dllyw9cL3S7LlFRBNBVyppQ224XnZeVQbK1OLXTklzApwRHdMMm6pFxm5JW5\nw/epf8rGMEkEjArPeJu2EXeScJwL0E3bYHMksgN81rIVeC6OytLQdBI85raqXgu+\n18jZ5TS8fRfnPy4guDKGZOqJrZUHil/PRh0ujaCSeHPewy65BljuCLD+Y48lFBqB\nsmWJAM3b0Zh7Wh/XZegO1ZvbRs1RSaPSbOqWVc5hHYqmbFoaSZSUxTX5deLJ1/U7\nWpCZxhlYP3JY7GmN7y655ahKBgucoWiGXhpphizATZ97y8GUk9xfqWFxiuROE7Y0\nSVXpeYFTCUv6FBduCljo95f6/r5eQDhe0zK/8cbo3NNY6PV1UhECggEBAO2bTlMc\nVxZUAnnqhqVaAqnLCKFwpQ0AcI8uQBfT0iEnPcxeFFXvzrOQYvxhWMMnJaLBNoT0\nUEEHUiaYyJXay+cuGj9EEK1Ym+Hf/fLvHaSKxwkE5GgR7wD+fKr68Q9wjE+qV6pU\nmBOvC4n9Yx+9qqTuU4hJGsUz7RSx1lDqtcNpQJ/PyOH+FeB2NmWBJDHVgHo8kr/E\nNnhCMS02LGiTZ2hGRM0T1hdJESVBjdt5pH7aFm92ATfFia/qmJBbIpdStgdMGrXT\ng3ERf1lNpy+JfHLGPfhcLBZE6nkKKkfHPqMhSAkzDLIkEo8M62DMwlVc6ENNQhco\nuAaO8mn0Y4ppURsCggEBAM864P3hcC9/5mT2KqOY6kTOgULrd2SPw8/ksrcvKbwv\nnHekqwd+G36e48fQ8VoTH3LMohwEtDZxVXJH9RISJz0a3/POABTLFHNlL6tUVH80\nsii9Ui5q/qHWUjhXrHaODVPOhKE4YiKS5bkzQfBtnEnRjj+wFInyh2DViNYntqTr\njyl5o/qFEE3jcR88yIOPqUoxQp+gNd7JoWyEugiALBuv6lbEVTCEnaiagQ9knffE\nvtafEeOmBIxFe1cPZlryPjpVTvp+fsOLo2SVadpKZXwVdusqzKq6Qy7SKuQ3JB8T\nqSPJbARh2Af+tj/TP7dhACVJunnMdLHj6UoEQY1/udUCggEBAKV0dEIjhfEiZucG\njOQvnZMmssfYbNZ06+yIRFFTSwuTC9F34alF6CXZ8VPVZb3fULHY4WILzqa5tlby\nJRtU1JAS5mwPtZ1ACqxrEadNItMlYBvDsFlXw5ppm95kB+C8ergu4gTWC2AJkHty\nWsgLhvx6iOhqH54a3oh+ncKS7ic476tZQYU9LXa0WoSsPMwG5AQ6keW/eD6duSjG\nhH39xxAxfk9f9MEaPDo5P9MJDabXrq/G+GohwBMjxUEgdSHykpbaQ+9BblIZ4RgK\nntTXZDV4HkcHyBtpuey3S2HYOKHX1xWw2rJhtBqw4Gbzro9KtuHGtrLxw9OtTtjB\nOV90nukCggEAIwHARkMSD4QcqIuJMm+/i3YTEfnzBlLe8jyhEB7wfvDQDsoLdk2Q\nWXh/5B5g1yWJ13vIGtGUm+nTVGXheXcFl+X1VRtEmj6gKso6Hkg1qfN98THiMyMx\n9qbc5tWwtapTiAfIEfATa+HC2uFUz0fE8hdrX0jgf1kwE2SfJAY7bWq5mXkKAWT/\nFxlH384F1WusXnMp3QtUbllDYrCdOJhmWxsBjDzIY1TTyUqoaLBHmzZqQplGjV1b\nQVLvfgqE8PIhHvuQRvKdeW0aYgB3jD+rGyYJG7r0LhcfCEbKphjGilVo6jm5fJgG\nxXr60JdgGRtSyfnFfZPTVsW4gB9t73hXLQKCAQAbOOiXyHLWdb7GkuZ+W7ZGQz87\nzL6db+BQ06Lj6u5h3P3GwcX/6vs1X2Zc06QSSISW2WMhpsedE3S2DXNWODjTPu4j\nAHnQz28DUw+pwzQlm2Ba7u94h6kDK5zoN4UFWo4rFpy1LExLrrBEUlr2FWyccvKJ\nBGz1NDVchjuj/reP3opKd2VutwiwyjsLoPPstXvFCeKfoIWq3jdBindL1CDxlDex\nS3N6yQNDqaF5eW4GHG3QQqQeRFSBtR9sX8bqiPwUopqUVflc2nVCg1Bas4p76qIF\nkOpcTCsJvdDvgEcK7hq4iMyjfDg35SfTOSvnkKoYkrdo+Bg+6dS1WMnFCh6a\n-----END RSA PRIVATE KEY-----\nEOK" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Running locally 4 | Start local site with: 5 | 6 | ```shell 7 | wp-env start 8 | ``` 9 | 10 | Change permalink structure: 11 | 12 | ```shell 13 | wp-env run cli "wp rewrite structure '/%postname%'" 14 | ``` 15 | 16 | The site is now available at http://localhost:8888. 17 | 18 | ## Releasing 19 | This section documents the process for issuing a new release. 20 | 21 | ### Create a PR for the release 22 | 23 | > You'll need [`jq`](https://stedolan.github.io/jq), which on macOS can be installed with `brew install jq`. 24 | 25 | > You'll need the [GitHub CLI](https://cli.github.com), which on macOS can be installed with `brew install gh`. 26 | 27 | > You'll also need to login to the GitHub CLI with `gh auth login`, if you haven't already. 28 | 29 | You can prepare a release with the following command (using `1.2.3` as example): 30 | 31 | > Make sure to consider [Semantic Versioning](https://semver.org) to decide which version you're issuing. 32 | 33 | ```shell 34 | bin/prepare-release.sh 1.2.3 35 | ``` 36 | 37 | A new draft PR will now have been created, and its branch checked out locally. 38 | 39 | ### Run the tests 40 | You must make sure tests pass before publishing a new release. See [End-to-end tests](tests/README.md) for instructions on running the tests. 41 | 42 | ### Add a Changelog 43 | A Changelog must be added to the `Changelog` section of `README.md`. In the PR description, you can find a link to all the commits since the previous release. You should manually go through the list and identify merged PRs that should be included in the Changelog (i.e. PRs that result in user-facing changes). 44 | 45 | You should push a commit with the new Changelog entry to the release branch, and then copy the Changelog to the PR description as well. 46 | 47 | ### Merge the PR 48 | Once you're satisfied with the release, you can merge the PR. The PR will be merged into the `main` branch, and a GitHub Action will be triggered which will create a **draft** release on GitHub. 49 | 50 | ### Publish the release 51 | Copy the Changelog entry from the PR description (or `README.md`) into the release description, then publish the release. Publishing the release will trigger a GitHub Action which will publish the release to the WordPress Plugin Directory. 52 | 53 | Go to the [plugin directory page](https://wordpress.org/plugins/openid-connect-server/) and make sure the new version is available. This might take up to a few minutes. 54 | 55 | ### Done 56 | The release has been published to both GitHub and the WordPress Plugin Directory. You can now delete the release branch from your local machine. 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenID Connect Server 2 | 3 | - Contributors: wordpressdotorg, akirk, ashfame, psrpinto 4 | - Tags: oidc, oauth, openid, openid connect, oauth server 5 | - Requires at least: 6.0 6 | - Tested up to: 6.8 7 | - Requires PHP: 7.4 8 | - License: [GPLv2](http://www.gnu.org/licenses/gpl-2.0.html) 9 | - Stable tag: 2.0.0 10 | - GitHub Plugin URI: https://github.com/Automattic/wp-openid-connect-server 11 | 12 | Use OpenID Connect to log in to other webservices using your own WordPress. 13 | 14 | ## Description 15 | 16 | With this plugin you can use your own WordPress install to authenticate with a webservice that provides [OpenID Connect](https://openid.net/connect/) to implement Single-Sign On (SSO) for your users. 17 | 18 | The plugin is currently only configured using constants and hooks as follows: 19 | 20 | ### Define the RSA keys 21 | 22 | If you don't have keys that you want to use yet, generate them using these commands: 23 | ~~~console 24 | openssl genrsa -out oidc.key 4096 25 | openssl rsa -in oidc.key -pubout -out public.key 26 | ~~~ 27 | 28 | And make them available to the plugin as follows (this needs to be added before WordPress loads): 29 | 30 | ~~~php 31 | define( 'OIDC_PUBLIC_KEY', << array( 59 | 'name' => 'The name of the Client', 60 | 'secret' => 'a secret string', 61 | 'redirect_uri' => 'https://example.com/redirect.uri', 62 | 'grant_types' => array( 'authorization_code' ), 63 | 'scope' => 'openid profile', 64 | ), 65 | ); 66 | } 67 | ~~~ 68 | 69 | ### Exclude URL from caching 70 | 71 | - `example.com/wp-json/openid-connect/userinfo`: We implement caching exclusion measures for this endpoint by setting `Cache-Control: 'no-cache'` headers and defining the `DONOTCACHEPAGE` constant. If you have a unique caching configuration, please ensure that you manually exclude this URL from caching. 72 | 73 | ### Github Repo 74 | You can report any issues you encounter directly on [Github repo: Automattic/wp-openid-connect-server](https://github.com/Automattic/wp-openid-connect-server) 75 | 76 | ## Changelog 77 | 78 | ### 2.0.0 79 | 80 | - [Breaking] Add a configuration option to support clients that don't require consent [#118](https://github.com/Automattic/wp-openid-connect-server/pull/118) props @lart2150 81 | - Make client_id and client_secret optional for the token endpoint [#116](https://github.com/Automattic/wp-openid-connect-server/pull/116) props @lart2150 82 | - Update expected args specs for token endpoint as per OIDC spec [#117](https://github.com/Automattic/wp-openid-connect-server/pull/117) 83 | 84 | ### 1.3.4 85 | 86 | - Add the autoloader to the uninstall script [#111](https://github.com/Automattic/wp-openid-connect-server/pull/111) props @MariaMozgunova 87 | 88 | ### 1.3.3 89 | 90 | - Fix failing login when Authorize form is non-English [[#108](https://github.com/Automattic/wp-openid-connect-server/pull/108)] 91 | - Improvements in site health tests for key detection [[#104](https://github.com/Automattic/wp-openid-connect-server/pull/104)][[#105](https://github.com/Automattic/wp-openid-connect-server/pull/105)] 92 | 93 | ### 1.3.2 94 | 95 | - Prevent userinfo endpoint from being cached [[#99](https://github.com/Automattic/wp-openid-connect-server/pull/99)] 96 | 97 | ### 1.3.0 98 | 99 | - Return `display_name` as the `name` property [[#87](https://github.com/Automattic/wp-openid-connect-server/pull/87)] 100 | - Change text domain to `openid-connect-server`, instead of `wp-openid-connect-server` [[#88](https://github.com/Automattic/wp-openid-connect-server/pull/88)] 101 | 102 | ### 1.2.1 103 | 104 | - No user facing changes 105 | 106 | ### 1.2.0 107 | 108 | - Add `oidc_user_claims` filter [[#82](https://github.com/Automattic/wp-openid-connect-server/pull/82)] 109 | -------------------------------------------------------------------------------- /bin/create-release-artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | shopt -s extglob 5 | 6 | function error { 7 | RED='\033[0;31m' 8 | NONE='\033[0m' 9 | printf "$RED$1$NONE\n" 10 | exit 1 11 | } 12 | 13 | if [ -z "$1" ]; then 14 | error "Provide a version, current version is $(jq '.version' composer.json)" 15 | fi 16 | 17 | VERSION=$1 18 | if [[ $VERSION == v* ]]; then 19 | # Strip leading v. 20 | VERSION="${VERSION:1}" 21 | fi 22 | 23 | RELEASE_ROOT_DIR="$(pwd)/release" 24 | RELEASE_DIR="$RELEASE_ROOT_DIR/$VERSION" 25 | rm -rf "$RELEASE_DIR" && mkdir -p "$RELEASE_DIR" 26 | 27 | PATHS_TO_INCLUDE=( 28 | "src" 29 | "openid-connect-server.php" 30 | "uninstall.php" 31 | "LICENSE" 32 | "README.md" 33 | ) 34 | for path in "${PATHS_TO_INCLUDE[@]}";do 35 | cp -r "$path" "$RELEASE_DIR/$path" 36 | done 37 | 38 | rm -rf "$RELEASE_DIR/build/.tmp" 39 | 40 | COMPOSER_VENDOR_DIR=vendor_release composer install --no-ansi --no-dev --no-interaction --no-plugins --no-scripts --optimize-autoloader 41 | mv vendor_release "$RELEASE_DIR/vendor" 42 | 43 | # Rename release directory from version name (e.g. 1.2.3) to `openid-connect-server` so that root directory in the artifacts is named `openid-connect-server`. 44 | # Then create the archives, and rename back to the versioned name (e.g. 1.2.3). 45 | rm -rf "$RELEASE_ROOT_DIR/openid-connect-server" 46 | mv "$RELEASE_DIR" "$RELEASE_ROOT_DIR/openid-connect-server" 47 | cd "$RELEASE_ROOT_DIR" 48 | zip -r "openid-connect-server-$VERSION.zip" openid-connect-server 49 | tar -cvzf "openid-connect-server-$VERSION.tar.gz" openid-connect-server 50 | mv "$RELEASE_ROOT_DIR/openid-connect-server" "$RELEASE_DIR" 51 | -------------------------------------------------------------------------------- /bin/prepare-release.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | set -e 4 | 5 | function error { 6 | RED='\033[0;31m' 7 | NONE='\033[0m' 8 | printf "$RED$1$NONE\n" 9 | exit 1 10 | } 11 | 12 | if [ -z "$1" ]; then 13 | error "Provide a new version, current version is $(jq '.version' composer.json)" 14 | fi 15 | 16 | VERSION=$1 17 | if [[ $VERSION == v* ]]; then 18 | # Strip leading v. 19 | VERSION="${VERSION:1}" 20 | fi 21 | 22 | RELEASE_BRANCH="release-$VERSION" 23 | 24 | CURRENT_BRANCH=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD) 25 | if [[ "$CURRENT_BRANCH" != "main" ]] && [[ "$CURRENT_BRANCH" != "$RELEASE_BRANCH" ]]; then 26 | error "You must be on branch main" 27 | fi 28 | 29 | # Make sure we're up-to-date with origin. 30 | git fetch 31 | git pull --ff-only origin main 32 | 33 | # Checkout or create branch for release. 34 | if [[ $(git branch --list "$RELEASE_BRANCH") ]] 35 | then 36 | git checkout "$RELEASE_BRANCH" 37 | else 38 | git checkout -b "$RELEASE_BRANCH" 39 | fi 40 | 41 | jq ".version = \"$VERSION\"" composer.json > composer.json.tmp 42 | mv composer.json.tmp composer.json 43 | git add composer.json 44 | 45 | sed -i"" -e "s/\(Version: \)\(.*\)/\1 $VERSION/g" openid-connect-server.php 46 | sed -i"" -e "s/\(Stable tag: \)\(.*\)/\1$VERSION/g" README.md 47 | rm -f openid-connect-server.php-e README.md-e 48 | git add openid-connect-server.php README.md 49 | 50 | # Show diff and ask for confirmation. 51 | git --no-pager diff --cached 52 | printf "\n\n" 53 | read -p "Would you like to commit, push and open a PR for the above diff? [y|n] " yn 54 | case $yn in 55 | yes|y ) 56 | echo "Ok, continuing";; 57 | * ) 58 | error "Exiting without committing." 59 | esac 60 | 61 | # Commit and push. 62 | git commit -m "Release $VERSION" 63 | git push -u origin "$RELEASE_BRANCH" 64 | 65 | # Open PR. 66 | LATEST_VERSION_TAG=$(git describe --tags --match "[0-9]*" --abbrev=0 HEAD) 67 | PR_BODY=$(cat <<-EOB 68 | [Commits since $LATEST_VERSION_TAG](https://github.com/Automattic/wp-openid-connect-server/compare/$LATEST_VERSION_TAG...$RELEASE_BRANCH) 69 | EOB 70 | ) 71 | gh pr create --draft --base main --label "Prepare Release" --title "Release $VERSION" --body "$PR_BODY" --assignee @me 72 | 73 | echo "A Pull Request has been created for Release $VERSION (see URL above)." 74 | echo "The release will automatically be created once the Pull Request is merged." 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "require": { 4 | "ext-json": "*", 5 | "ext-openssl": "*", 6 | "bshaffer/oauth2-server-php": "^1.12" 7 | }, 8 | "require-dev": { 9 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", 10 | "johnpbloch/wordpress": "^6.0", 11 | "squizlabs/php_codesniffer": "^3.7", 12 | "wp-coding-standards/wpcs": "dev-develop" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "OpenIDConnectServer\\": "src/" 17 | } 18 | }, 19 | "scripts": { 20 | "phpcs": "phpcs --standard=phpcs.xml", 21 | "test": "cd tests && yarn && yarn start" 22 | }, 23 | "config": { 24 | "sort-packages": true, 25 | "allow-plugins": { 26 | "johnpbloch/wordpress-core-installer": false, 27 | "dealerdirect/phpcodesniffer-composer-installer": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "257b3de136d77efe602378ca957b979a", 8 | "packages": [ 9 | { 10 | "name": "bshaffer/oauth2-server-php", 11 | "version": "v1.13.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/bshaffer/oauth2-server-php.git", 15 | "reference": "cd11527b29ceb340f24015b6df868c22908bcf12" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/bshaffer/oauth2-server-php/zipball/cd11527b29ceb340f24015b6df868c22908bcf12", 20 | "reference": "cd11527b29ceb340f24015b6df868c22908bcf12", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.1" 25 | }, 26 | "require-dev": { 27 | "aws/aws-sdk-php": "^2.8", 28 | "firebase/php-jwt": "^2.2", 29 | "mongodb/mongodb": "^1.1", 30 | "phpunit/phpunit": "^7.5||^8.0", 31 | "predis/predis": "^1.1", 32 | "thobbs/phpcassa": "dev-master", 33 | "yoast/phpunit-polyfills": "^1.0" 34 | }, 35 | "suggest": { 36 | "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage", 37 | "firebase/php-jwt": "~2.2 is required to use JWT features", 38 | "mongodb/mongodb": "^1.1 is required to use MongoDB storage", 39 | "predis/predis": "Required to use Redis storage", 40 | "thobbs/phpcassa": "Required to use Cassandra storage" 41 | }, 42 | "type": "library", 43 | "autoload": { 44 | "psr-0": { 45 | "OAuth2": "src/" 46 | } 47 | }, 48 | "notification-url": "https://packagist.org/downloads/", 49 | "license": [ 50 | "MIT" 51 | ], 52 | "authors": [ 53 | { 54 | "name": "Brent Shaffer", 55 | "email": "bshafs@gmail.com", 56 | "homepage": "http://brentertainment.com" 57 | } 58 | ], 59 | "description": "OAuth2 Server for PHP", 60 | "homepage": "http://github.com/bshaffer/oauth2-server-php", 61 | "keywords": [ 62 | "auth", 63 | "oauth", 64 | "oauth2" 65 | ], 66 | "support": { 67 | "issues": "https://github.com/bshaffer/oauth2-server-php/issues", 68 | "source": "https://github.com/bshaffer/oauth2-server-php/tree/v1.13.0" 69 | }, 70 | "time": "2022-10-12T17:33:08+00:00" 71 | } 72 | ], 73 | "packages-dev": [ 74 | { 75 | "name": "dealerdirect/phpcodesniffer-composer-installer", 76 | "version": "v0.7.2", 77 | "source": { 78 | "type": "git", 79 | "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", 80 | "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" 81 | }, 82 | "dist": { 83 | "type": "zip", 84 | "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", 85 | "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", 86 | "shasum": "" 87 | }, 88 | "require": { 89 | "composer-plugin-api": "^1.0 || ^2.0", 90 | "php": ">=5.3", 91 | "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" 92 | }, 93 | "require-dev": { 94 | "composer/composer": "*", 95 | "php-parallel-lint/php-parallel-lint": "^1.3.1", 96 | "phpcompatibility/php-compatibility": "^9.0" 97 | }, 98 | "type": "composer-plugin", 99 | "extra": { 100 | "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" 101 | }, 102 | "autoload": { 103 | "psr-4": { 104 | "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" 105 | } 106 | }, 107 | "notification-url": "https://packagist.org/downloads/", 108 | "license": [ 109 | "MIT" 110 | ], 111 | "authors": [ 112 | { 113 | "name": "Franck Nijhof", 114 | "email": "franck.nijhof@dealerdirect.com", 115 | "homepage": "http://www.frenck.nl", 116 | "role": "Developer / IT Manager" 117 | }, 118 | { 119 | "name": "Contributors", 120 | "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" 121 | } 122 | ], 123 | "description": "PHP_CodeSniffer Standards Composer Installer Plugin", 124 | "homepage": "http://www.dealerdirect.com", 125 | "keywords": [ 126 | "PHPCodeSniffer", 127 | "PHP_CodeSniffer", 128 | "code quality", 129 | "codesniffer", 130 | "composer", 131 | "installer", 132 | "phpcbf", 133 | "phpcs", 134 | "plugin", 135 | "qa", 136 | "quality", 137 | "standard", 138 | "standards", 139 | "style guide", 140 | "stylecheck", 141 | "tests" 142 | ], 143 | "support": { 144 | "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", 145 | "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" 146 | }, 147 | "time": "2022-02-04T12:51:07+00:00" 148 | }, 149 | { 150 | "name": "johnpbloch/wordpress", 151 | "version": "6.2.0", 152 | "source": { 153 | "type": "git", 154 | "url": "https://github.com/johnpbloch/wordpress.git", 155 | "reference": "de3b59e7e724bdc6c334f93ddbf2f9e06a92267b" 156 | }, 157 | "dist": { 158 | "type": "zip", 159 | "url": "https://api.github.com/repos/johnpbloch/wordpress/zipball/de3b59e7e724bdc6c334f93ddbf2f9e06a92267b", 160 | "reference": "de3b59e7e724bdc6c334f93ddbf2f9e06a92267b", 161 | "shasum": "" 162 | }, 163 | "require": { 164 | "johnpbloch/wordpress-core": "6.2.0", 165 | "johnpbloch/wordpress-core-installer": "^1.0 || ^2.0", 166 | "php": ">=5.6.20" 167 | }, 168 | "type": "package", 169 | "notification-url": "https://packagist.org/downloads/", 170 | "license": [ 171 | "GPL-2.0+" 172 | ], 173 | "authors": [ 174 | { 175 | "name": "WordPress Community", 176 | "homepage": "https://wordpress.org/about/" 177 | } 178 | ], 179 | "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", 180 | "homepage": "https://wordpress.org/", 181 | "keywords": [ 182 | "blog", 183 | "cms", 184 | "wordpress" 185 | ], 186 | "support": { 187 | "docs": "https://developer.wordpress.org/", 188 | "forum": "https://wordpress.org/support/", 189 | "irc": "irc://irc.freenode.net/wordpress", 190 | "issues": "https://core.trac.wordpress.org/", 191 | "source": "https://core.trac.wordpress.org/browser" 192 | }, 193 | "time": "2023-03-30T04:14:55+00:00" 194 | }, 195 | { 196 | "name": "johnpbloch/wordpress-core", 197 | "version": "6.2.0", 198 | "source": { 199 | "type": "git", 200 | "url": "https://github.com/johnpbloch/wordpress-core.git", 201 | "reference": "cec139b6b71913343b68fbf043c468407a921225" 202 | }, 203 | "dist": { 204 | "type": "zip", 205 | "url": "https://api.github.com/repos/johnpbloch/wordpress-core/zipball/cec139b6b71913343b68fbf043c468407a921225", 206 | "reference": "cec139b6b71913343b68fbf043c468407a921225", 207 | "shasum": "" 208 | }, 209 | "require": { 210 | "ext-json": "*", 211 | "php": ">=5.6.20" 212 | }, 213 | "provide": { 214 | "wordpress/core-implementation": "6.2.0" 215 | }, 216 | "type": "wordpress-core", 217 | "notification-url": "https://packagist.org/downloads/", 218 | "license": [ 219 | "GPL-2.0-or-later" 220 | ], 221 | "authors": [ 222 | { 223 | "name": "WordPress Community", 224 | "homepage": "https://wordpress.org/about/" 225 | } 226 | ], 227 | "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", 228 | "homepage": "https://wordpress.org/", 229 | "keywords": [ 230 | "blog", 231 | "cms", 232 | "wordpress" 233 | ], 234 | "support": { 235 | "forum": "https://wordpress.org/support/", 236 | "irc": "irc://irc.freenode.net/wordpress", 237 | "issues": "https://core.trac.wordpress.org/", 238 | "source": "https://core.trac.wordpress.org/browser", 239 | "wiki": "https://codex.wordpress.org/" 240 | }, 241 | "time": "2023-03-30T04:14:50+00:00" 242 | }, 243 | { 244 | "name": "johnpbloch/wordpress-core-installer", 245 | "version": "2.0.0", 246 | "source": { 247 | "type": "git", 248 | "url": "https://github.com/johnpbloch/wordpress-core-installer.git", 249 | "reference": "237faae9a60a4a2e1d45dce1a5836ffa616de63e" 250 | }, 251 | "dist": { 252 | "type": "zip", 253 | "url": "https://api.github.com/repos/johnpbloch/wordpress-core-installer/zipball/237faae9a60a4a2e1d45dce1a5836ffa616de63e", 254 | "reference": "237faae9a60a4a2e1d45dce1a5836ffa616de63e", 255 | "shasum": "" 256 | }, 257 | "require": { 258 | "composer-plugin-api": "^1.0 || ^2.0", 259 | "php": ">=5.6.0" 260 | }, 261 | "conflict": { 262 | "composer/installers": "<1.0.6" 263 | }, 264 | "require-dev": { 265 | "composer/composer": "^1.0 || ^2.0", 266 | "phpunit/phpunit": ">=5.7.27" 267 | }, 268 | "type": "composer-plugin", 269 | "extra": { 270 | "class": "johnpbloch\\Composer\\WordPressCorePlugin" 271 | }, 272 | "autoload": { 273 | "psr-0": { 274 | "johnpbloch\\Composer\\": "src/" 275 | } 276 | }, 277 | "notification-url": "https://packagist.org/downloads/", 278 | "license": [ 279 | "GPL-2.0-or-later" 280 | ], 281 | "authors": [ 282 | { 283 | "name": "John P. Bloch", 284 | "email": "me@johnpbloch.com" 285 | } 286 | ], 287 | "description": "A custom installer to handle deploying WordPress with composer", 288 | "keywords": [ 289 | "wordpress" 290 | ], 291 | "support": { 292 | "issues": "https://github.com/johnpbloch/wordpress-core-installer/issues", 293 | "source": "https://github.com/johnpbloch/wordpress-core-installer/tree/master" 294 | }, 295 | "time": "2020-04-16T21:44:57+00:00" 296 | }, 297 | { 298 | "name": "phpcsstandards/phpcsextra", 299 | "version": "1.0.3", 300 | "source": { 301 | "type": "git", 302 | "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", 303 | "reference": "7029c051cd310e2e17c6caea3429bfbe290c41ae" 304 | }, 305 | "dist": { 306 | "type": "zip", 307 | "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/7029c051cd310e2e17c6caea3429bfbe290c41ae", 308 | "reference": "7029c051cd310e2e17c6caea3429bfbe290c41ae", 309 | "shasum": "" 310 | }, 311 | "require": { 312 | "php": ">=5.4", 313 | "phpcsstandards/phpcsutils": "^1.0", 314 | "squizlabs/php_codesniffer": "^3.7.1" 315 | }, 316 | "require-dev": { 317 | "php-parallel-lint/php-console-highlighter": "^1.0", 318 | "php-parallel-lint/php-parallel-lint": "^1.3.2", 319 | "phpcsstandards/phpcsdevcs": "^1.1.5", 320 | "phpcsstandards/phpcsdevtools": "^1.2.0", 321 | "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0" 322 | }, 323 | "type": "phpcodesniffer-standard", 324 | "extra": { 325 | "branch-alias": { 326 | "dev-stable": "1.x-dev", 327 | "dev-develop": "1.x-dev" 328 | } 329 | }, 330 | "notification-url": "https://packagist.org/downloads/", 331 | "license": [ 332 | "LGPL-3.0-or-later" 333 | ], 334 | "authors": [ 335 | { 336 | "name": "Juliette Reinders Folmer", 337 | "homepage": "https://github.com/jrfnl", 338 | "role": "lead" 339 | }, 340 | { 341 | "name": "Contributors", 342 | "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" 343 | } 344 | ], 345 | "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", 346 | "keywords": [ 347 | "PHP_CodeSniffer", 348 | "phpcbf", 349 | "phpcodesniffer-standard", 350 | "phpcs", 351 | "standards", 352 | "static analysis" 353 | ], 354 | "support": { 355 | "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", 356 | "source": "https://github.com/PHPCSStandards/PHPCSExtra" 357 | }, 358 | "time": "2023-03-28T17:48:27+00:00" 359 | }, 360 | { 361 | "name": "phpcsstandards/phpcsutils", 362 | "version": "1.0.5", 363 | "source": { 364 | "type": "git", 365 | "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", 366 | "reference": "0cfef5193e68e8ff179333d8ae937db62939b656" 367 | }, 368 | "dist": { 369 | "type": "zip", 370 | "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/0cfef5193e68e8ff179333d8ae937db62939b656", 371 | "reference": "0cfef5193e68e8ff179333d8ae937db62939b656", 372 | "shasum": "" 373 | }, 374 | "require": { 375 | "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", 376 | "php": ">=5.4", 377 | "squizlabs/php_codesniffer": "^3.7.1 || 4.0.x-dev@dev" 378 | }, 379 | "require-dev": { 380 | "ext-filter": "*", 381 | "php-parallel-lint/php-console-highlighter": "^1.0", 382 | "php-parallel-lint/php-parallel-lint": "^1.3.2", 383 | "phpcsstandards/phpcsdevcs": "^1.1.3", 384 | "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3", 385 | "yoast/phpunit-polyfills": "^1.0.1" 386 | }, 387 | "type": "phpcodesniffer-standard", 388 | "extra": { 389 | "branch-alias": { 390 | "dev-stable": "1.x-dev", 391 | "dev-develop": "1.x-dev" 392 | } 393 | }, 394 | "autoload": { 395 | "classmap": [ 396 | "PHPCSUtils/" 397 | ] 398 | }, 399 | "notification-url": "https://packagist.org/downloads/", 400 | "license": [ 401 | "LGPL-3.0-or-later" 402 | ], 403 | "authors": [ 404 | { 405 | "name": "Juliette Reinders Folmer", 406 | "homepage": "https://github.com/jrfnl", 407 | "role": "lead" 408 | }, 409 | { 410 | "name": "Contributors", 411 | "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" 412 | } 413 | ], 414 | "description": "A suite of utility functions for use with PHP_CodeSniffer", 415 | "homepage": "https://phpcsutils.com/", 416 | "keywords": [ 417 | "PHP_CodeSniffer", 418 | "phpcbf", 419 | "phpcodesniffer-standard", 420 | "phpcs", 421 | "phpcs3", 422 | "standards", 423 | "static analysis", 424 | "tokens", 425 | "utility" 426 | ], 427 | "support": { 428 | "docs": "https://phpcsutils.com/", 429 | "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", 430 | "source": "https://github.com/PHPCSStandards/PHPCSUtils" 431 | }, 432 | "time": "2023-04-17T16:27:27+00:00" 433 | }, 434 | { 435 | "name": "squizlabs/php_codesniffer", 436 | "version": "3.7.2", 437 | "source": { 438 | "type": "git", 439 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 440 | "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" 441 | }, 442 | "dist": { 443 | "type": "zip", 444 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", 445 | "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", 446 | "shasum": "" 447 | }, 448 | "require": { 449 | "ext-simplexml": "*", 450 | "ext-tokenizer": "*", 451 | "ext-xmlwriter": "*", 452 | "php": ">=5.4.0" 453 | }, 454 | "require-dev": { 455 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 456 | }, 457 | "bin": [ 458 | "bin/phpcs", 459 | "bin/phpcbf" 460 | ], 461 | "type": "library", 462 | "extra": { 463 | "branch-alias": { 464 | "dev-master": "3.x-dev" 465 | } 466 | }, 467 | "notification-url": "https://packagist.org/downloads/", 468 | "license": [ 469 | "BSD-3-Clause" 470 | ], 471 | "authors": [ 472 | { 473 | "name": "Greg Sherwood", 474 | "role": "lead" 475 | } 476 | ], 477 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 478 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", 479 | "keywords": [ 480 | "phpcs", 481 | "standards", 482 | "static analysis" 483 | ], 484 | "support": { 485 | "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", 486 | "source": "https://github.com/squizlabs/PHP_CodeSniffer", 487 | "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" 488 | }, 489 | "time": "2023-02-22T23:07:41+00:00" 490 | }, 491 | { 492 | "name": "wp-coding-standards/wpcs", 493 | "version": "dev-develop", 494 | "source": { 495 | "type": "git", 496 | "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", 497 | "reference": "fca9d9ef2dcd042658ccb9df16552f5048e7bb04" 498 | }, 499 | "dist": { 500 | "type": "zip", 501 | "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/fca9d9ef2dcd042658ccb9df16552f5048e7bb04", 502 | "reference": "fca9d9ef2dcd042658ccb9df16552f5048e7bb04", 503 | "shasum": "" 504 | }, 505 | "require": { 506 | "ext-filter": "*", 507 | "php": ">=5.4", 508 | "phpcsstandards/phpcsextra": "^1.0", 509 | "phpcsstandards/phpcsutils": "^1.0.5", 510 | "squizlabs/php_codesniffer": "^3.7.2" 511 | }, 512 | "require-dev": { 513 | "php-parallel-lint/php-console-highlighter": "^1.0.0", 514 | "php-parallel-lint/php-parallel-lint": "^1.3.2", 515 | "phpcompatibility/php-compatibility": "^9.0", 516 | "phpcsstandards/phpcsdevtools": "^1.2.0", 517 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 518 | }, 519 | "suggest": { 520 | "ext-mbstring": "For improved results" 521 | }, 522 | "default-branch": true, 523 | "type": "phpcodesniffer-standard", 524 | "notification-url": "https://packagist.org/downloads/", 525 | "license": [ 526 | "MIT" 527 | ], 528 | "authors": [ 529 | { 530 | "name": "Contributors", 531 | "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" 532 | } 533 | ], 534 | "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", 535 | "keywords": [ 536 | "phpcs", 537 | "standards", 538 | "static analysis", 539 | "wordpress" 540 | ], 541 | "support": { 542 | "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", 543 | "source": "https://github.com/WordPress/WordPress-Coding-Standards", 544 | "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" 545 | }, 546 | "time": "2023-05-01T08:34:06+00:00" 547 | } 548 | ], 549 | "aliases": [], 550 | "minimum-stability": "stable", 551 | "stability-flags": { 552 | "wp-coding-standards/wpcs": 20 553 | }, 554 | "prefer-stable": false, 555 | "prefer-lowest": false, 556 | "platform": { 557 | "ext-json": "*", 558 | "ext-openssl": "*" 559 | }, 560 | "platform-dev": [], 561 | "plugin-api-version": "2.3.0" 562 | } 563 | -------------------------------------------------------------------------------- /openid-connect-server.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Coding Style Checks. 4 | 5 | ./openid-connect-server.php 6 | ./src/ 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Http/Handlers/AuthenticateHandler.php: -------------------------------------------------------------------------------- 1 | consent_storage = $consent_storage; 18 | $this->clients = $clients; 19 | } 20 | 21 | public function handle( Request $request, Response $response ): Response { 22 | if ( ! is_user_logged_in() ) { 23 | auth_redirect(); 24 | } 25 | 26 | $client_id = $request->query( 'client_id' ); 27 | 28 | $client_name = $this->clients->getClientName( $client_id ); 29 | if ( empty( $client_name ) ) { 30 | $response->setStatusCode( 404 ); 31 | 32 | return $response; 33 | } 34 | 35 | if ( 36 | ! $this->clients->clientRequiresConsent( $client_id ) 37 | || ! $this->consent_storage->needs_consent( get_current_user_id(), $client_id ) 38 | ) { 39 | $this->redirect( $request ); 40 | // TODO: return response instead of exiting. 41 | exit; 42 | } 43 | 44 | $data = array( 45 | 'user' => wp_get_current_user(), 46 | 'client_name' => $client_name, 47 | 'body_class_attr' => implode( ' ', array_diff( get_body_class(), array( 'error404' ) ) ), 48 | 'cancel_url' => $this->get_cancel_url( $request ), 49 | 'form_url' => Router::make_rest_url( 'authorize' ), 50 | 'form_fields' => $request->getAllQueryParameters(), 51 | ); 52 | 53 | $has_permission = current_user_can( apply_filters( 'oidc_minimal_capability', OIDC_DEFAULT_MINIMAL_CAPABILITY ) ); 54 | if ( ! $has_permission ) { 55 | login_header( 'OIDC Connect', null, new \WP_Error( 'OIDC_NO_PERMISSION', __( "You don't have permission to use OpenID Connect.", 'openid-connect-server' ) ) ); 56 | $this->render_no_permission_screen( $data ); 57 | } else { 58 | login_header( 'OIDC Connect' ); 59 | $this->render_consent_screen( $data ); 60 | } 61 | 62 | login_footer(); 63 | 64 | return $response; 65 | } 66 | 67 | private function render_no_permission_screen( $data ) { 68 | ?> 69 |
70 | 95 |
96 | 101 |
102 | 146 |
147 | wp_create_nonce( 'wp_rest' ) ), 156 | $request->getAllQueryParameters() 157 | ), 158 | Router::make_rest_url( 'authorize' ) 159 | ) 160 | ); 161 | } 162 | 163 | private function get_cancel_url( Request $request ) { 164 | return add_query_arg( 165 | array( 166 | 'error' => 'access_denied', 167 | 'error_description' => 'Access denied! Permission not granted.', 168 | 'state' => $request->query( 'state' ), 169 | ), 170 | $request->query( 'redirect_uri' ), 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Http/Handlers/AuthorizeHandler.php: -------------------------------------------------------------------------------- 1 | server = $server; 23 | $this->consent_storage = $consent_storage; 24 | $this->clients = $clients; 25 | } 26 | 27 | public function handle( Request $request, Response $response ): Response { 28 | // Our dependency bshaffer's OAuth library currently has a bug where it doesn't pick up nonce correctly if it's a POST request to the Authorize endpoint. 29 | // Fix has been contributed upstream (https://github.com/bshaffer/oauth2-server-php/pull/1032) but it doesn't look it would be merged anytime soon based on recent activity. 30 | // Hence, as a temporary fix, we are copying over the nonce from parsed $_POST values to parsed $_GET values in $request object here. 31 | if ( isset( $request->request['nonce'] ) && ! isset( $request->query['nonce'] ) ) { 32 | $request->query['nonce'] = $request->request['nonce']; 33 | } 34 | 35 | if ( ! $this->server->validateAuthorizeRequest( $request, $response ) ) { 36 | return $response; 37 | } 38 | 39 | // The initial OIDC request will come without a nonce, thus unauthenticated. 40 | if ( ! is_user_logged_in() || ! current_user_can( apply_filters( 'oidc_minimal_capability', OIDC_DEFAULT_MINIMAL_CAPABILITY ) ) ) { 41 | // This is handled by a hook in wp-login.php which will display a form asking the user to consent. 42 | // TODO: Redirect with $response->setRedirect(). 43 | wp_safe_redirect( add_query_arg( array_map( 'rawurlencode', array_merge( $request->getAllQueryParameters(), array( 'action' => 'openid-authenticate' ) ) ), wp_login_url() ) ); 44 | exit; 45 | } 46 | 47 | $user = wp_get_current_user(); 48 | 49 | $client_id = $request->query( 'client_id', $request->request( 'client_id' ) ); 50 | if ( 51 | $this->clients->clientRequiresConsent( $client_id ) 52 | && $this->consent_storage->needs_consent( $user->ID, $client_id ) 53 | ) { 54 | if ( ! isset( $_POST['authorize'] ) || __( 'Authorize', 'openid-connect-server' ) !== $_POST['authorize'] ) { 55 | $response->setError( 403, 'user_authorization_required', 'This application requires your consent.' ); 56 | return $response; 57 | } 58 | 59 | $this->consent_storage->update_timestamp( $user->ID, $client_id ); 60 | } 61 | 62 | return $this->server->handleAuthorizeRequest( $request, $response, true, $user->user_login ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Http/Handlers/ConfigurationHandler.php: -------------------------------------------------------------------------------- 1 | addHttpHeaders( 13 | array( 14 | 'Content-type' => 'application/json', 15 | 'Access-Control-Allow-Origin' => '*', 16 | ) 17 | ); 18 | 19 | $response->addParameters( $this->configuration() ); 20 | 21 | return $response; 22 | } 23 | 24 | private function configuration(): array { 25 | return array( 26 | 'issuer' => Router::make_url(), 27 | 'jwks_uri' => Router::make_url( '.well-known/jwks.json' ), 28 | 'authorization_endpoint' => Router::make_rest_url( 'authorize' ), 29 | 'token_endpoint' => Router::make_rest_url( 'token' ), 30 | 'userinfo_endpoint' => Router::make_rest_url( 'userinfo' ), 31 | 'scopes_supported' => array( 'openid', 'profile' ), 32 | 'response_types_supported' => array( 'code' ), 33 | 'id_token_signing_alg_values_supported' => array( 'RS256' ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Http/Handlers/TokenHandler.php: -------------------------------------------------------------------------------- 1 | server = $server; 15 | } 16 | 17 | public function handle( Request $request, Response $response ): Response { 18 | return $this->server->handleTokenRequest( $request ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Http/Handlers/UserInfoHandler.php: -------------------------------------------------------------------------------- 1 | server = $server; 15 | } 16 | 17 | public function handle( Request $request, Response $response ): Response { 18 | // prevent caching plugins from caching this page. 19 | if ( ! defined( 'DONOTCACHEPAGE' ) ) { 20 | define( 'DONOTCACHEPAGE', true ); 21 | } 22 | 23 | $response = $this->server->handleUserInfoRequest( $request, $response ); 24 | $response->addHttpHeaders( 25 | array( 26 | 'Cache-Control' => 'no-store', 27 | 'Pragma' => 'no-cache', 28 | ) 29 | ); 30 | 31 | return $response; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Handlers/WebKeySetsHandler.php: -------------------------------------------------------------------------------- 1 | public_key = $public_key; 14 | } 15 | 16 | public function handle( Request $request, Response $response ): Response { 17 | $response->addHttpHeaders( 18 | array( 19 | 'Content-type' => 'application/json', 20 | 'Access-Control-Allow-Origin' => '*', 21 | ) 22 | ); 23 | 24 | $response->addParameters( array( 'keys' => array( $this->key_info() ) ) ); 25 | 26 | return $response; 27 | } 28 | 29 | private function key_info(): array { 30 | $key = openssl_pkey_get_details( openssl_pkey_get_public( $this->public_key ) ); 31 | 32 | return array( 33 | 'kty' => 'RSA', 34 | 'use' => 'sig', 35 | 'alg' => 'RS256', 36 | // phpcs:ignore 37 | 'n' => rtrim( strtr( base64_encode( $key['rsa']['n'] ), '+/', '-_' ), '=' ), 38 | // phpcs:ignore 39 | 'e' => rtrim( strtr( base64_encode( $key['rsa']['e'] ), '+/', '-_' ), '=' ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/RequestHandler.php: -------------------------------------------------------------------------------- 1 | rest_routes[ $route ] ) ) { 29 | return; 30 | } 31 | 32 | $this->routes[ $route ] = $handler; 33 | } 34 | 35 | public function add_rest_route( string $route, RequestHandler $handler, array $methods = array( 'GET' ), array $args = array() ) { 36 | $route_with_prefix = self::PREFIX . "/$route"; 37 | if ( isset( $this->rest_routes[ $route_with_prefix ] ) ) { 38 | return; 39 | } 40 | 41 | $this->rest_routes[ $route_with_prefix ] = $handler; 42 | 43 | add_action( 44 | 'rest_api_init', 45 | function () use ( $route, $methods, $args ) { 46 | register_rest_route( 47 | self::PREFIX, 48 | $route, 49 | array( 50 | 'methods' => $methods, 51 | 'permission_callback' => '__return_true', 52 | 'callback' => array( $this, 'handle_rest_request' ), 53 | 'args' => $args, 54 | ) 55 | ); 56 | } 57 | ); 58 | } 59 | 60 | private function get_current_route(): string { 61 | $wp_url = get_site_url(); 62 | $installed_dir = wp_parse_url( $wp_url, PHP_URL_PATH ); 63 | 64 | // Requested URI relative to WP install. 65 | $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; 66 | $uri = str_replace( $installed_dir, '', $uri ); 67 | 68 | $route = strtok( $uri, '?' ); 69 | 70 | return trim( $route, '/' ); 71 | } 72 | 73 | /** 74 | * This method is meant for internal use in this class only. 75 | * It must not be used elsewhere. 76 | * It's only public since it's used as a callback. 77 | */ 78 | public function handle_request() { 79 | if ( empty( $_SERVER['REQUEST_URI'] ) ) { 80 | return; 81 | } 82 | 83 | $route = $this->get_current_route(); 84 | 85 | if ( ! isset( $this->routes[ $route ] ) ) { 86 | return; 87 | } 88 | 89 | $handler = $this->routes[ $route ]; 90 | $this->do_handle_request( $handler ); 91 | } 92 | 93 | /** 94 | * This method is meant for internal use in this class only. 95 | * It must not be used elsewhere. 96 | * It's only public since it's used as a callback. 97 | */ 98 | public function handle_rest_request( $wp_request ) { 99 | $route = $wp_request->get_route(); 100 | // Remove leading slashes. 101 | $route = ltrim( $route, '/' ); 102 | 103 | if ( ! isset( $this->rest_routes[ $route ] ) ) { 104 | $response = new Response(); 105 | $response->setStatusCode( 404 ); 106 | $response->send(); 107 | exit; 108 | } 109 | 110 | $handler = $this->rest_routes[ $route ]; 111 | $this->do_handle_request( $handler ); 112 | } 113 | 114 | private function do_handle_request( RequestHandler $handler ) { 115 | $request = Request::createFromGlobals(); 116 | $response = new Response(); 117 | $response = $handler->handle( $request, $response ); 118 | $response->send(); 119 | exit(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/OpenIDConnectServer.php: -------------------------------------------------------------------------------- 1 | public_key = $public_key; 29 | $this->clients = new ClientCredentialsStorage( $clients ); 30 | $this->router = new Router(); 31 | $this->consent_storage = new ConsentStorage(); 32 | 33 | $config = array( 34 | 'use_jwt_access_tokens' => true, 35 | 'use_openid_connect' => true, 36 | 'issuer' => home_url( '/' ), 37 | ); 38 | 39 | $server = new Server( new AuthorizationCodeStorage(), $config ); 40 | $server->addStorage( new PublicKeyStorage( $public_key, $private_key ), 'public_key' ); 41 | $server->addStorage( $this->clients, 'client_credentials' ); 42 | $server->addStorage( new UserClaimsStorage(), 'user_claims' ); 43 | 44 | // Declare rest routes. 45 | $this->router->add_rest_route( 46 | 'token', 47 | new TokenHandler( $server ), 48 | array( 'POST' ), 49 | $this->expected_arguments_specification( 'token' ), 50 | ); 51 | $this->router->add_rest_route( 52 | 'authorize', 53 | new AuthorizeHandler( $server, $this->consent_storage, $this->clients ), 54 | array( 'GET', 'POST' ), 55 | $this->expected_arguments_specification( 'authorize' ), 56 | ); 57 | $this->router->add_rest_route( 58 | 'userinfo', 59 | new UserInfoHandler( $server ), 60 | array( 'GET', 'POST' ), 61 | $this->expected_arguments_specification( 'userinfo' ), 62 | ); 63 | 64 | // Declare non-rest routes. 65 | $this->router->add_route( '.well-known/jwks.json', new WebKeySetsHandler( $this->public_key ) ); 66 | $this->router->add_route( '.well-known/openid-configuration', new ConfigurationHandler() ); 67 | add_action( 'login_form_openid-authenticate', array( $this, 'authenticate_handler' ) ); 68 | 69 | // Cleanup. 70 | $this->setup_cron_hook(); 71 | } 72 | 73 | public function authenticate_handler() { 74 | $request = Request::createFromGlobals(); 75 | $response = new Response(); 76 | 77 | $authenticate_handler = new AuthenticateHandler( $this->consent_storage, $this->clients ); 78 | $authenticate_handler->handle( $request, $response ); 79 | exit; 80 | } 81 | 82 | private function expected_arguments_specification( $route ) { 83 | switch ( $route ) { 84 | case 'authorize': 85 | return array( 86 | 'client_id' => array( 87 | 'type' => 'string', 88 | 'required' => true, 89 | ), 90 | 'redirect_uri' => array( 91 | 'type' => 'string', 92 | ), 93 | 'response_type' => array( 94 | 'type' => 'string', 95 | 'required' => true, 96 | ), 97 | 'state' => array( 98 | 'type' => 'string', 99 | ), 100 | ); 101 | case 'token': 102 | return array( 103 | 'grant_type' => array( 104 | 'type' => 'string', 105 | 'required' => true, 106 | ), 107 | 'client_id' => array( 108 | 'type' => 'string', 109 | 'required' => false, 110 | ), 111 | 'client_secret' => array( 112 | 'type' => 'string', 113 | 'required' => false, 114 | ), 115 | 'client_assertion' => array( 116 | 'type' => 'string', 117 | 'required' => false, 118 | ), 119 | 'client_assertion_type' => array( 120 | 'type' => 'string', 121 | 'required' => false, 122 | ), 123 | 'redirect_uri' => array( 124 | 'type' => 'string', 125 | 'required' => true, 126 | ), 127 | 'code' => array( 128 | 'type' => 'string', 129 | 'required' => true, 130 | ), 131 | ); 132 | case 'userinfo': 133 | return array(); 134 | } 135 | } 136 | 137 | public function setup_cron_hook() { 138 | if ( ! wp_next_scheduled( 'oidc_cron_hook' ) ) { 139 | wp_schedule_event( time(), 'weekly', 'oidc_cron_hook' ); 140 | } 141 | } 142 | 143 | /** 144 | * This function is invoked from uninstall.php 145 | * 146 | * As of v1.0 we have two things that are being stored and should be removed on uninstall: 147 | * 1) Consent storage 148 | * 2) Auth code storage 149 | */ 150 | public static function uninstall() { 151 | ConsentStorage::uninstall(); 152 | AuthorizationCodeStorage::uninstall(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/SiteStatusTests.php: -------------------------------------------------------------------------------- 1 | __( 'The public key is defined and in the right format', 'openid-connect-server' ), 13 | 'test' => array( $this, 'site_status_test_public_key' ), 14 | ); 15 | 16 | $tests['direct']['oidc-private-key'] = array( 17 | 'label' => __( 'The private key is defined and in the right format', 'openid-connect-server' ), 18 | 'test' => array( $this, 'site_status_test_private_key' ), 19 | ); 20 | 21 | $tests['direct']['oidc-clients'] = array( 22 | 'label' => __( 'One or more clients have been defined correctly', 'openid-connect-server' ), 23 | 'test' => array( $this, 'site_status_test_clients' ), 24 | ); 25 | 26 | return $tests; 27 | } 28 | 29 | public function site_status_test_public_key(): array { 30 | $key_is_defined = defined( 'OIDC_PUBLIC_KEY' ); 31 | $key_has_valid_pem_headers = (bool) preg_match( 32 | '/^-----BEGIN\s.*PUBLIC KEY-----.*-----END\s.*PUBLIC KEY-----$/s', 33 | OIDC_PUBLIC_KEY 34 | ); 35 | 36 | if ( ! $key_is_defined ) { 37 | $label = __( 'The public key constant OIDC_PUBLIC_KEY is not defined.', 'openid-connect-server' ); 38 | $status = 'critical'; 39 | $badge = 'red'; 40 | } elseif ( $key_has_valid_pem_headers ) { 41 | $label = __( 'The public key is defined and in the right format', 'openid-connect-server' ); 42 | $status = 'good'; 43 | $badge = 'green'; 44 | } else { 45 | $label = __( 'The public key constant OIDC_PUBLIC_KEY is malformed.', 'openid-connect-server' ); 46 | $status = 'critical'; 47 | $badge = 'red'; 48 | } 49 | 50 | return array( 51 | 'label' => wp_kses_post( $label ), 52 | 'status' => $status, 53 | 'badge' => array( 54 | 'label' => __( 'OpenID Connect Server', 'openid-connect-server' ), 55 | 'color' => $badge, 56 | ), 57 | 'description' => 58 | '

' . 59 | __( 'You need to provide RSA keys for the OpenID Connect Server to function.', 'openid-connect-server' ) . 60 | ' ' . 61 | wp_kses_post( 62 | sprintf( 63 | // Translators: %s is a URL. 64 | __( "Please see the plugin's readme file for details.", 'openid-connect-server' ), 65 | '"https://github.com/Automattic/wp-openid-connect-server/blob/main/README.md"' 66 | ) 67 | ) . 68 | '

', 69 | 'test' => 'oidc-public-key', 70 | ); 71 | } 72 | 73 | public function site_status_test_private_key(): array { 74 | $key_is_defined = defined( 'OIDC_PRIVATE_KEY' ); 75 | $key_has_valid_pem_headers = (bool) preg_match( 76 | '/^-----BEGIN\s.*PRIVATE KEY-----.*-----END\s.*PRIVATE KEY-----$/s', 77 | OIDC_PRIVATE_KEY 78 | ); 79 | 80 | if ( ! $key_is_defined ) { 81 | $label = __( 'The private key constant OIDC_PRIVATE_KEY is not defined.', 'openid-connect-server' ); 82 | $status = 'critical'; 83 | $badge = 'red'; 84 | } elseif ( $key_has_valid_pem_headers ) { 85 | $label = __( 'The private key is defined and in the right format', 'openid-connect-server' ); 86 | $status = 'good'; 87 | $badge = 'green'; 88 | } else { 89 | $label = __( 'The private key constant OIDC_PRIVATE_KEY is malformed.', 'openid-connect-server' ); 90 | $status = 'critical'; 91 | $badge = 'red'; 92 | } 93 | 94 | return array( 95 | 'label' => wp_kses_post( $label ), 96 | 'status' => $status, 97 | 'badge' => array( 98 | 'label' => __( 'OpenID Connect Server', 'openid-connect-server' ), 99 | 'color' => $badge, 100 | ), 101 | 'description' => 102 | '

' . 103 | __( 'You need to provide RSA keys for the OpenID Connect Server to function.', 'openid-connect-server' ) . 104 | ' ' . 105 | wp_kses_post( 106 | sprintf( 107 | // translators: %s is a URL. 108 | __( "Please see the plugin's readme file for details.", 'openid-connect-server' ), 109 | '"https://github.com/Automattic/wp-openid-connect-server/blob/trunk/README.md"' 110 | ) 111 | ) . 112 | '

', 113 | 'test' => 'oidc-private-key', 114 | ); 115 | } 116 | 117 | public function site_status_test_clients(): array { 118 | $clients = apply_filters( 'oidc_registered_clients', array() ); 119 | if ( empty( $clients ) ) { 120 | $label = __( 'No clients have been defined.', 'openid-connect-server' ); 121 | $status = 'critical'; 122 | $badge = 'red'; 123 | } else { 124 | $all_clients_ok = true; 125 | foreach ( $clients as $client_id => $client ) { 126 | $error = false; 127 | if ( strlen( $client_id ) < 10 ) { 128 | $error = __( 'The client id (array key) needs to be a random string.', 'openid-connect-server' ); 129 | } 130 | if ( empty( $client['redirect_uri'] ) ) { 131 | $error = __( 'You need to specify a redirect_uri.', 'openid-connect-server' ); 132 | } 133 | if ( ! preg_match( '#^https://#', $client['redirect_uri'] ) ) { 134 | $error = __( 'The redirect_uri needs to be a HTTPS URL.', 'openid-connect-server' ); 135 | } 136 | if ( empty( $client['name'] ) ) { 137 | $error = __( 'You need to specify a name.', 'openid-connect-server' ); 138 | } 139 | if ( $error ) { 140 | $label = sprintf( // translators: %s is a random string representing the client id. 141 | __( 'The client %1$s seems to be malformed. %2$s', 'openid-connect-server' ), 142 | $client_id, 143 | $error 144 | ); 145 | 146 | $status = 'critical'; 147 | $badge = 'red'; 148 | $all_clients_ok = false; 149 | break; 150 | } 151 | } 152 | 153 | if ( $all_clients_ok ) { 154 | $label = _n( 'The defined client seems to be in the right format', 'The defined clients seem to be in the right format', count( $clients ), 'openid-connect-server' ); 155 | $status = 'good'; 156 | $badge = 'green'; 157 | } 158 | } 159 | 160 | return array( 161 | 'label' => wp_kses_post( $label ), 162 | 'status' => $status, 163 | 'badge' => array( 164 | 'label' => __( 'OpenID Connect Server', 'openid-connect-server' ), 165 | 'color' => $badge, 166 | ), 167 | 'description' => 168 | '

' . 169 | __( 'You need to define clients for the OpenID Connect Server to function.', 'openid-connect-server' ) . 170 | ' ' . 171 | wp_kses_post( 172 | sprintf( 173 | // Translators: %s is a URL. 174 | __( "Please see the plugin's readme file for details.", 'openid-connect-server' ), 175 | '"https://github.com/Automattic/wp-openid-connect-server/blob/trunk/README.md"' 176 | ) 177 | ) . 178 | '

', 179 | 'test' => 'oidc-clients', 180 | ); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Storage/AuthorizationCodeStorage.php: -------------------------------------------------------------------------------- 1 | 'string', // client identifier. 12 | 'redirect_uri' => 'string', // redirect URI. 13 | 'expires' => 'int', // expires as unix timestamp. 14 | 'scope' => 'string', // scope as space-separated string. 15 | 'id_token' => 'string', // The OpenID Connect id_token. 16 | ); 17 | 18 | public function __construct() { 19 | add_action( 'oidc_cron_hook', array( $this, 'cleanupOldCodes' ) ); 20 | } 21 | 22 | private function getUserIdByCode( $code ) { 23 | if ( empty( $code ) ) { 24 | return null; 25 | } 26 | 27 | $users = get_users( 28 | array( 29 | 'number' => 1, 30 | // Specifying blog_id does nothing for non-MultiSite installs. But for MultiSite installs, it allows you 31 | // to customize users of which site is supposed to be available for whatever sites 32 | // this plugin is meant to be activated on. 33 | 'blog_id' => apply_filters( 'oidc_auth_code_storage_blog_id', get_current_blog_id() ), 34 | // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 35 | 'meta_key' => self::META_KEY_PREFIX . '_client_id_' . $code, 36 | // Using a meta_key EXISTS query is not slow, see https://github.com/WordPress/WordPress-Coding-Standards/issues/1871. 37 | 'meta_compare' => 'EXISTS', 38 | ) 39 | ); 40 | 41 | if ( empty( $users ) ) { 42 | return null; 43 | } 44 | 45 | return absint( $users[0]->ID ); 46 | } 47 | 48 | public function getAuthorizationCode( $code ) { 49 | $user_id = $this->getUserIdByCode( $code ); 50 | if ( empty( $user_id ) ) { 51 | return null; 52 | } 53 | 54 | $user = new \WP_User( $user_id ); 55 | 56 | $authorization_code = array( 57 | 'user_id' => $user->user_login, 58 | 'code' => $code, 59 | ); 60 | foreach ( array_keys( self::$authorization_code_data ) as $key ) { 61 | $authorization_code[ $key ] = get_user_meta( $user_id, self::META_KEY_PREFIX . '_' . $key . '_' . $code, true ); 62 | } 63 | 64 | return $authorization_code; 65 | } 66 | 67 | public function setAuthorizationCode( $code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null ) { 68 | if ( empty( $code ) ) { 69 | return; 70 | } 71 | 72 | $user = get_user_by( 'login', $user_id ); // We have chosen WordPress' user_login as the user identifier for OIDC context. 73 | 74 | if ( $user ) { 75 | foreach ( self::$authorization_code_data as $key => $data_type ) { 76 | if ( 'int' === $data_type ) { 77 | $value = absint( $$key ); 78 | } else { 79 | $value = sanitize_text_field( $$key ); 80 | } 81 | 82 | update_user_meta( $user->ID, self::META_KEY_PREFIX . '_' . $key . '_' . $code, $value ); 83 | } 84 | } 85 | } 86 | 87 | public function expireAuthorizationCode( $code ) { 88 | $user_id = $this->getUserIdByCode( $code ); 89 | if ( empty( $user_id ) ) { 90 | return null; 91 | } 92 | 93 | foreach ( array_keys( self::$authorization_code_data ) as $key ) { 94 | delete_user_meta( $user_id, self::META_KEY_PREFIX . '_' . $key . '_' . $code ); 95 | } 96 | } 97 | 98 | /** 99 | * This function cleans up auth codes that are sitting in the database because of interrupted/abandoned OAuth flows. 100 | */ 101 | public function cleanupOldCodes() { 102 | global $wpdb; 103 | 104 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 105 | $data = $wpdb->get_results( 106 | $wpdb->prepare( 107 | "SELECT user_id, meta_key FROM $wpdb->usermeta WHERE meta_key LIKE %s AND meta_value < %d", 108 | 'oidc_expires_%', 109 | time() - 3600 // wait for an hour past expiry, to offer a chance at debug. 110 | ) 111 | ); 112 | if ( empty( $data ) ) { 113 | return; 114 | } 115 | 116 | foreach ( $data as $row ) { 117 | $code = substr( $row->meta_key, strlen( 'oidc_expires_' ) ); 118 | foreach ( array_keys( self::$authorization_code_data ) as $key ) { 119 | delete_user_meta( $row->user_id, self::META_KEY_PREFIX . '_' . $key . '_' . $code ); 120 | } 121 | } 122 | } 123 | 124 | public static function uninstall() { 125 | global $wpdb; 126 | 127 | // Following query is only possible via a direct query since meta_key is not a fixed string 128 | // and since it only runs at uninstall, we don't need it cached. 129 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 130 | $data = $wpdb->get_results( 131 | $wpdb->prepare( 132 | "SELECT user_id, meta_key FROM $wpdb->usermeta WHERE meta_key LIKE %s", 133 | 'oidc_expires_%', 134 | ) 135 | ); 136 | if ( empty( $data ) ) { 137 | return; 138 | } 139 | 140 | foreach ( $data as $row ) { 141 | $code = substr( $row->meta_key, strlen( 'oidc_expires_' ) ); 142 | foreach ( array_keys( self::$authorization_code_data ) as $key ) { 143 | delete_user_meta( $row->user_id, self::META_KEY_PREFIX . '_' . $key . '_' . $code ); 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Storage/ClientCredentialsStorage.php: -------------------------------------------------------------------------------- 1 | clients = $clients; 12 | } 13 | 14 | public function getClientDetails( $client_id ) { 15 | if ( ! $this->has( $client_id ) ) { 16 | return false; 17 | } 18 | 19 | $client = $this->get( $client_id ); 20 | 21 | return array( 22 | 'client_id' => $client_id, 23 | 'redirect_uri' => $client['redirect_uri'], 24 | 'scope' => $client['scope'], 25 | ); 26 | } 27 | 28 | public function getClientName( $client_id ) { 29 | if ( ! $this->has( $client_id ) ) { 30 | return ''; 31 | } 32 | 33 | $client = $this->get( $client_id ); 34 | 35 | if ( empty( $client['name'] ) ) { 36 | return ''; 37 | } 38 | 39 | return $client['name']; 40 | } 41 | 42 | public function clientRequiresConsent( $client_id ): bool { 43 | if ( ! $this->has( $client_id ) ) { 44 | return true; 45 | } 46 | 47 | $client = $this->get( $client_id ); 48 | 49 | if ( ! array_key_exists( 'requires_consent', $client ) ) { 50 | return true; 51 | } 52 | 53 | return false !== $client['requires_consent']; 54 | } 55 | 56 | public function getClientScope( $client_id ) { 57 | if ( ! $this->has( $client_id ) ) { 58 | return ''; 59 | } 60 | 61 | $client = $this->get( $client_id ); 62 | 63 | if ( ! isset( $client['scope'] ) ) { 64 | return ''; 65 | } 66 | 67 | return $client['scope']; 68 | } 69 | 70 | public function checkRestrictedGrantType( $client_id, $grant_type ) { 71 | if ( ! $this->has( $client_id ) ) { 72 | return false; 73 | } 74 | 75 | $client = $this->get( $client_id ); 76 | 77 | if ( ! isset( $client['grant_types'] ) ) { 78 | return false; 79 | } 80 | 81 | return in_array( $grant_type, $client['grant_types'], true ); 82 | } 83 | 84 | public function checkClientCredentials( $client_id, $client_secret = null ) { 85 | if ( ! $this->has( $client_id ) ) { 86 | return false; 87 | } 88 | 89 | $client = $this->get( $client_id ); 90 | 91 | if ( empty( $client['secret'] ) ) { 92 | return true; 93 | } 94 | 95 | return $client_secret === $client['secret']; 96 | } 97 | 98 | public function isPublicClient( $client_id ) { 99 | if ( ! $this->has( $client_id ) ) { 100 | return false; 101 | } 102 | 103 | $client = $this->get( $client_id ); 104 | 105 | return empty( $client['secret'] ); 106 | } 107 | 108 | private function get( $client_id ) { 109 | if ( ! $this->has( $client_id ) ) { 110 | return null; 111 | } 112 | 113 | return $this->clients[ $client_id ]; 114 | } 115 | 116 | private function has( $client_id ): bool { 117 | return isset( $this->clients[ $client_id ] ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Storage/ConsentStorage.php: -------------------------------------------------------------------------------- 1 | get_meta_key( $client_id ), true ) ); 16 | 17 | $past_consent_expiry = time() > ( $consent_timestamp + ( STICKY_CONSENT_DURATION ) ); 18 | 19 | return empty( $consent_timestamp ) || $past_consent_expiry; 20 | } 21 | 22 | public function update_timestamp( $user_id, $client_id ) { 23 | update_user_meta( $user_id, $this->get_meta_key( $client_id ), time() ); 24 | } 25 | 26 | public static function uninstall() { 27 | global $wpdb; 28 | 29 | // Following query is only possible via a direct query since meta_key is not a fixed string 30 | // and since it only runs at uninstall, we don't need it cached. 31 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 32 | $data = $wpdb->get_results( 33 | $wpdb->prepare( 34 | "SELECT user_id, meta_key FROM $wpdb->usermeta WHERE meta_key LIKE %s", 35 | self::META_KEY_PREFIX . '%', 36 | ) 37 | ); 38 | if ( empty( $data ) ) { 39 | return; 40 | } 41 | 42 | foreach ( $data as $row ) { 43 | delete_user_meta( $row->user_id, $row->meta_key ); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Storage/PublicKeyStorage.php: -------------------------------------------------------------------------------- 1 | public_key = $public_key; 13 | $this->private_key = $private_key; 14 | } 15 | 16 | public function getPublicKey( $client_id = null ) { 17 | return $this->public_key; 18 | } 19 | 20 | public function getPrivateKey( $client_id = null ) { 21 | return $this->private_key; 22 | } 23 | 24 | public function getEncryptionAlgorithm( $client_id = null ) { 25 | return 'RS256'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Storage/UserClaimsStorage.php: -------------------------------------------------------------------------------- 1 | $scope, 17 | ); 18 | 19 | if ( ! empty( $_REQUEST['nonce'] ) ) { 20 | $claims['nonce'] = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ); 21 | } 22 | 23 | $scopes = explode( ' ', $scope ); 24 | if ( ! in_array( 'profile', $scopes, true ) ) { 25 | return $claims; 26 | } 27 | 28 | $user = get_user_by( 'login', $user_login ); 29 | if ( ! $user ) { 30 | return $claims; 31 | } 32 | 33 | $field_map = array( 34 | 'username' => 'user_login', 35 | 'name' => 'display_name', 36 | 'given_name' => 'first_name', 37 | 'family_name' => 'last_name', 38 | 'nickname' => 'user_nicename', 39 | ); 40 | 41 | foreach ( $field_map as $key => $value ) { 42 | if ( $user->$value ) { 43 | $claims[ $key ] = $user->$value; 44 | } 45 | } 46 | 47 | $claims['picture'] = get_avatar_url( $user->user_email ); 48 | 49 | return apply_filters( 'oidc_user_claims', $claims, $user ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/.env: -------------------------------------------------------------------------------- 1 | ISSUER_URL="https://localhost:8443/" 2 | CLIENT_ID="oidc-server-plugin-tests" 3 | CLIENT_SECRET="oidc-server-plugin-tests" 4 | TLS_CA_CERT="../../matrix-oidc-playground/tls/ca/rootCA.pem" 5 | TLS_CERT="../../matrix-oidc-playground/tls/tls.pem" 6 | TLS_KEY="../../matrix-oidc-playground/tls/tls-key.pem" 7 | APP_BASE_URL="https://localhost:7443" 8 | WORDPRESS_USER="admin" 9 | WORDPRESS_PASS="password" 10 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn 2 | /node_modules 3 | .env.local 4 | -------------------------------------------------------------------------------- /tests/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-1.22.19.cjs 2 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # End-to-end tests 2 | 3 | Running theses tests requires having [matrix-oidc-playground](https://github.com/Automattic/matrix-oidc-playground/) running in the same machine as the tests. Make sure to follow the setup instructions there before running the tests. 4 | 5 | Once you have matrix-oidc-playground running, simply run: 6 | 7 | ```shell 8 | composer test 9 | ``` 10 | 11 | The tests pass when the output ends with something like: 12 | 13 | 14 | ```shell 15 | JWT token { 16 | iss: 'https://localhost:8443/', 17 | sub: 'admin', 18 | aud: 'oidc-server-plugin-tests', 19 | iat: 1695316090, 20 | exp: 1695319690, 21 | auth_time: 1695316090, 22 | nonce: '7926217c4ad37e6db5cc8e6f78a421ed' 23 | } 24 | userinfo { 25 | scope: 'openid profile', 26 | username: 'admin', 27 | name: 'admin', 28 | nickname: 'admin', 29 | picture: 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=96&d=mm&r=g', 30 | sub: 'admin' 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /tests/env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | ISSUER_URL: string, 5 | CLIENT_ID: string, 6 | CLIENT_SECRET: string, 7 | TLS_CA_CERT: string, 8 | TLS_CERT: string, 9 | TLS_KEY: string, 10 | APP_BASE_URL: string, 11 | WORDPRESS_USER: string, 12 | WORDPRESS_PASS: string, 13 | } 14 | } 15 | } 16 | 17 | export {} 18 | -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import dotenv from "dotenv" 4 | import {OpenIdClient} from "./src/OpenIdClient"; 5 | import {HttpsServer} from "./src/HttpsServer"; 6 | import {HttpsClient} from "./src/HttpsClient"; 7 | import crypto from "crypto"; 8 | import { parse as parseHtml } from 'node-html-parser'; 9 | import {AxiosResponse} from "axios"; 10 | 11 | dotenv.config({ path: ".env" }); 12 | if (fs.existsSync(".env.local")) { 13 | dotenv.config({ path: ".env.local", override: true }); 14 | } 15 | 16 | let httpsServer: HttpsServer; 17 | 18 | async function run() { 19 | const env = process.env; 20 | if (!env.ISSUER_URL || !env.CLIENT_ID || !env.CLIENT_SECRET || !env.TLS_CA_CERT || !env.TLS_CERT || !env.TLS_KEY || !env.APP_BASE_URL || !env.WORDPRESS_USER || !env.WORDPRESS_PASS) { 21 | console.error("Some or all required environment variables were not defined. Set them in the .env file."); 22 | process.exit(1); 23 | } 24 | 25 | const caCert = fs.readFileSync(path.resolve(env.TLS_CA_CERT)); 26 | 27 | const openIdClient = new OpenIdClient({ 28 | issuerUrl: env.ISSUER_URL, 29 | clientId: env.CLIENT_ID, 30 | clientSecret: env.CLIENT_SECRET, 31 | redirectUri: env.APP_BASE_URL, 32 | caCert, 33 | }); 34 | 35 | const httpsClient = new HttpsClient({ 36 | caCert, 37 | }) 38 | 39 | httpsServer = new HttpsServer({ 40 | baseUrl: new URL(env.APP_BASE_URL), 41 | tlsCert: fs.readFileSync(path.resolve(env.TLS_CERT)), 42 | tlsKey: fs.readFileSync(path.resolve(env.TLS_KEY)), 43 | }); 44 | httpsServer.start(); 45 | 46 | const state = crypto.randomBytes(16).toString("hex"); 47 | const nonce = crypto.randomBytes(16).toString("hex"); 48 | 49 | // Generate authorization URL. 50 | const authorizationUrl = await openIdClient.authorizationUrl(state, nonce); 51 | 52 | // Call authorization URL. 53 | let response = await httpsClient.get(authorizationUrl); 54 | let responseUrl = new URL(response.config.url ?? ""); 55 | 56 | // Get promise to next server request. 57 | let serverRequest = httpsServer.once(); 58 | 59 | // Log in. 60 | if (response.status === 200 && responseUrl.toString().includes("wp-login.php")) { 61 | response = await httpsClient.post(new URL(`${env.ISSUER_URL}/wp-login.php`), { 62 | testcookie: "1", 63 | log: env.WORDPRESS_USER, 64 | pwd: env.WORDPRESS_PASS, 65 | redirect_to: responseUrl.searchParams.get("redirect_to"), 66 | }); 67 | } 68 | 69 | // Grant authorization. 70 | await grantAuthorization(httpsClient, env.ISSUER_URL ?? "", response); 71 | 72 | // Get access token. 73 | const request = await serverRequest; 74 | const tokenSet = await openIdClient.exchangeCodeForToken(request); 75 | const jwt = parseJwt(tokenSet.id_token ?? ""); 76 | console.log("JWT token", jwt); 77 | 78 | // Get userinfo. 79 | const userinfo = await openIdClient.userinfo(tokenSet.access_token ?? ""); 80 | console.debug("userinfo", userinfo); 81 | 82 | // Check JWT token. 83 | if (jwt.iss !== env.ISSUER_URL) { 84 | throw `JWT token iss doesn't match. Expected '${env.ISSUER_URL}', got '${jwt.iss}'`; 85 | } 86 | if (jwt.sub !== env.WORDPRESS_USER) { 87 | throw `JWT token sub doesn't match. Expected '${env.WORDPRESS_USER}', got '${jwt.sub}'`; 88 | } 89 | if (jwt.aud !== env.CLIENT_ID) { 90 | throw `JWT token aud doesn't match. Expected '${env.CLIENT_ID}', got '${jwt.aud}'`; 91 | } 92 | 93 | // Check userinfo response. 94 | if (userinfo.scope !== "openid profile") { 95 | throw `Userinfo scope doesn't match. Expected 'openid profile', got '${userinfo.scope}'`; 96 | } 97 | if (userinfo.sub !== env.WORDPRESS_USER) { 98 | throw `Userinfo sub doesn't match. Expected ${env.WORDPRESS_USER}, got '${userinfo.sub}'`; 99 | } 100 | 101 | console.info("Tests passed"); 102 | } 103 | 104 | async function grantAuthorization(httpsClient: HttpsClient, issuerUrl: string, response: AxiosResponse): Promise { 105 | const authorizeButtonMarkup = ''; 106 | if (response.status !== 200 || !response.data.includes(authorizeButtonMarkup)) { 107 | // Nothing to do, we were not shown the grant authorization screen. 108 | return response; 109 | } 110 | 111 | const html = parseHtml(response.data); 112 | let inputFields = html.querySelector("form")?.querySelectorAll("input"); 113 | if (!inputFields || inputFields.length === 0) { 114 | throw "Authorization form not found"; 115 | } 116 | 117 | inputFields = inputFields.filter(field => ["hidden", "submit"].includes(field.attrs.type)); 118 | const params = {}; 119 | // @ts-ignore 120 | inputFields.forEach(field => params[field.attrs.name] = field.attrs.value); 121 | 122 | return httpsClient.post(new URL(`${issuerUrl}/wp-json/openid-connect/authorize`), params); 123 | } 124 | 125 | function parseJwt(token: string) { 126 | const base64Url = token.split('.')[1]; 127 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); 128 | const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { 129 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 130 | }).join('')); 131 | return JSON.parse(jsonPayload); 132 | } 133 | 134 | void run().catch(error => { 135 | console.error("Tests failed:", error); 136 | process.exit(1); 137 | }).finally(() => { 138 | if (httpsServer) { 139 | void httpsServer.stop(); 140 | } 141 | }); 142 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "yarn@1.22.19", 3 | "name": "openid-connect-server-integration-tests", 4 | "version": "1.0.0", 5 | "main": "index.ts", 6 | "license": "MIT", 7 | "type": "commonjs", 8 | "scripts": { 9 | "start": "ts-node-esm index.ts" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "^20.6.2", 13 | "ts-node": "^10.9.1", 14 | "typescript": "^5.2.2" 15 | }, 16 | "dependencies": { 17 | "axios": "^1.5.0", 18 | "dotenv": "^16.3.1", 19 | "http-terminator": "^3.2.0", 20 | "node-html-parser": "^6.1.10", 21 | "openid-client": "^5.5.0", 22 | "set-cookie-parser": "^2.6.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/src/HttpsClient.ts: -------------------------------------------------------------------------------- 1 | import https from "node:https"; 2 | import axios, {AxiosInstance, AxiosResponse} from "axios"; 3 | const setCookieParser = require('set-cookie-parser'); 4 | 5 | type Options = { 6 | caCert: Buffer, 7 | } 8 | 9 | export class HttpsClient { 10 | private readonly axios: AxiosInstance; 11 | private cookies: string[] = []; 12 | 13 | constructor(private readonly options: Options) { 14 | this.axios = axios.create({ 15 | httpsAgent: new https.Agent({ ca: this.options.caCert }), 16 | maxRedirects: 0, 17 | validateStatus: function (status) { 18 | return [200, 302].includes(status); 19 | } 20 | }); 21 | 22 | this.axios.interceptors.response.use(response => { 23 | console.debug("response", response.status, response.config.url, "\n") 24 | return response; 25 | }); 26 | 27 | this.axios.interceptors.request.use(request => { 28 | console.debug("request", request.url, request.data ?? "", "\n") 29 | return request; 30 | }); 31 | } 32 | 33 | async get(url: URL): Promise { 34 | const response = await this.axios.get(url.toString(), { 35 | headers: { 36 | Cookie: this.cookieHeader(), 37 | }, 38 | }); 39 | 40 | this.setCookies(response); 41 | 42 | if (response.status === 302) { 43 | return this.get(response.headers.location); 44 | } 45 | 46 | return response; 47 | } 48 | 49 | async post(url: URL, data: object): Promise { 50 | const formData = new FormData(); 51 | for (const property in data) { 52 | // @ts-ignore 53 | formData.append(property, data[property]); 54 | } 55 | 56 | const response = await this.axios.post(url.toString(), formData, { 57 | headers: { 58 | Cookie: this.cookieHeader(), 59 | }, 60 | }); 61 | 62 | this.setCookies(response); 63 | 64 | if (response.status === 302) { 65 | return this.get(response.headers.location); 66 | } 67 | 68 | return response 69 | } 70 | 71 | private setCookies(response: AxiosResponse) { 72 | const cookies = setCookieParser.parse(response); 73 | for (const cookie of cookies) { 74 | this.cookies[cookie.name] = cookie.value; 75 | } 76 | } 77 | 78 | private cookieHeader(): string { 79 | let header = ""; 80 | for (const name in this.cookies) { 81 | const value = this.cookies[name]; 82 | if (value.trim() === "") { 83 | continue; 84 | } 85 | if (header !== "") { 86 | header += "; "; 87 | } 88 | header += `${encodeURIComponent(name)}=${encodeURIComponent(this.cookies[name])}`; 89 | } 90 | return header; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/src/HttpsServer.ts: -------------------------------------------------------------------------------- 1 | import http, {IncomingMessage, ServerResponse} from "node:http"; 2 | import https, {Server as BaseServer} from "node:https"; 3 | import {createHttpTerminator, HttpTerminator} from "http-terminator"; 4 | 5 | type Options = { 6 | baseUrl: URL, 7 | tlsCert: Buffer, 8 | tlsKey: Buffer, 9 | }; 10 | 11 | export class HttpsServer { 12 | private readonly server: BaseServer; 13 | private readonly terminator: HttpTerminator; 14 | 15 | constructor(private readonly options: Options) { 16 | this.server = https.createServer({ 17 | key: options.tlsKey, 18 | cert: options.tlsCert, 19 | }); 20 | this.terminator = createHttpTerminator({server: this.server}); 21 | } 22 | 23 | async once(): Promise { 24 | return new Promise((resolve, reject) => { 25 | this.server.once("error", error => reject(error)); 26 | this.server.once("request", (request, response) => { 27 | response.end(); 28 | resolve(request); 29 | }); 30 | }); 31 | } 32 | 33 | start() { 34 | // @ts-ignore 35 | this.server.listen(this.options.baseUrl.port, this.options.baseUrl.hostname, () => { 36 | console.info(`Server listening at ${this.options.baseUrl.toString()}`); 37 | }); 38 | } 39 | 40 | async stop() { 41 | this.server.removeAllListeners(); 42 | void this.terminator.terminate(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/src/OpenIdClient.ts: -------------------------------------------------------------------------------- 1 | import {Client, custom as openidOptions, Issuer, TokenSet, UserinfoResponse} from "openid-client"; 2 | import {IncomingMessage} from "node:http"; 3 | 4 | type Options = { 5 | issuerUrl: string, 6 | clientId: string, 7 | clientSecret: string, 8 | redirectUri: string, 9 | caCert: Buffer, 10 | }; 11 | 12 | export class OpenIdClient { 13 | private issuer?: Issuer; 14 | private client?: Client; 15 | 16 | constructor(private readonly options: Options) { 17 | openidOptions.setHttpOptionsDefaults({ 18 | ca: options.caCert, 19 | }); 20 | } 21 | 22 | async authorizationUrl(state: string, nonce: string): Promise { 23 | await this.init(); 24 | const url = this.client?.authorizationUrl({ 25 | scope: "openid profile", 26 | state, 27 | nonce, 28 | }); 29 | 30 | return new URL(url ?? ""); 31 | } 32 | 33 | async exchangeCodeForToken(request: IncomingMessage): Promise { 34 | const params = this.client?.callbackParams(request); 35 | if (!params) { 36 | throw "Failed to parse callback params"; 37 | } 38 | 39 | const tokenSet = await this.client?.grant({ 40 | grant_type: "authorization_code", 41 | code: params.code, 42 | client_id: this.options.clientId, 43 | client_secret: this.options.clientId, 44 | redirect_uri: this.options.redirectUri, 45 | }); 46 | 47 | if (!tokenSet) { 48 | throw "Failed to get token set"; 49 | } 50 | 51 | return tokenSet; 52 | } 53 | 54 | async userinfo(token: string): Promise { 55 | const response = this.client?.userinfo(token); 56 | if (!response) { 57 | throw "Failed to get userinfo"; 58 | } 59 | return response 60 | } 61 | 62 | private async init() { 63 | if (this.issuer) { 64 | return; 65 | } 66 | this.issuer = await Issuer.discover(this.options.issuerUrl); 67 | console.debug('Discovered issuer %s %O', this.issuer.issuer, this.issuer.metadata, "\n"); 68 | 69 | this.client = new this.issuer.Client({ 70 | client_id: this.options.clientId, 71 | client_secret: this.options.clientId, 72 | redirect_uris: [this.options.redirectUri], 73 | response_types: ["code"], 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "NodeNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cspotcode/source-map-support@^0.8.0": 6 | version "0.8.1" 7 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" 8 | integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== 9 | dependencies: 10 | "@jridgewell/trace-mapping" "0.3.9" 11 | 12 | "@fastify/deepmerge@^1.0.0": 13 | version "1.3.0" 14 | resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a" 15 | integrity sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A== 16 | 17 | "@jridgewell/resolve-uri@^3.0.3": 18 | version "3.1.1" 19 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" 20 | integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== 21 | 22 | "@jridgewell/sourcemap-codec@^1.4.10": 23 | version "1.4.15" 24 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" 25 | integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== 26 | 27 | "@jridgewell/trace-mapping@0.3.9": 28 | version "0.3.9" 29 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" 30 | integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== 31 | dependencies: 32 | "@jridgewell/resolve-uri" "^3.0.3" 33 | "@jridgewell/sourcemap-codec" "^1.4.10" 34 | 35 | "@tsconfig/node10@^1.0.7": 36 | version "1.0.9" 37 | resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" 38 | integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== 39 | 40 | "@tsconfig/node12@^1.0.7": 41 | version "1.0.11" 42 | resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" 43 | integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== 44 | 45 | "@tsconfig/node14@^1.0.0": 46 | version "1.0.3" 47 | resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" 48 | integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== 49 | 50 | "@tsconfig/node16@^1.0.2": 51 | version "1.0.4" 52 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" 53 | integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== 54 | 55 | "@types/node@^20.6.2": 56 | version "20.6.2" 57 | resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.2.tgz#a065925409f59657022e9063275cd0b9bd7e1b12" 58 | integrity sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw== 59 | 60 | acorn-walk@^8.1.1: 61 | version "8.2.0" 62 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" 63 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 64 | 65 | acorn@^8.4.1: 66 | version "8.10.0" 67 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" 68 | integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== 69 | 70 | ajv-formats@^2.1.1: 71 | version "2.1.1" 72 | resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" 73 | integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== 74 | dependencies: 75 | ajv "^8.0.0" 76 | 77 | ajv@^8.0.0, ajv@^8.10.0: 78 | version "8.12.0" 79 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" 80 | integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== 81 | dependencies: 82 | fast-deep-equal "^3.1.1" 83 | json-schema-traverse "^1.0.0" 84 | require-from-string "^2.0.2" 85 | uri-js "^4.2.2" 86 | 87 | arg@^4.1.0: 88 | version "4.1.3" 89 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 90 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 91 | 92 | asynckit@^0.4.0: 93 | version "0.4.0" 94 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 95 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 96 | 97 | axios@^1.5.0: 98 | version "1.5.0" 99 | resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" 100 | integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ== 101 | dependencies: 102 | follow-redirects "^1.15.0" 103 | form-data "^4.0.0" 104 | proxy-from-env "^1.1.0" 105 | 106 | boolbase@^1.0.0: 107 | version "1.0.0" 108 | resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" 109 | integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== 110 | 111 | boolean@^3.1.4: 112 | version "3.2.0" 113 | resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" 114 | integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== 115 | 116 | combined-stream@^1.0.8: 117 | version "1.0.8" 118 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 119 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 120 | dependencies: 121 | delayed-stream "~1.0.0" 122 | 123 | create-require@^1.1.0: 124 | version "1.1.1" 125 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 126 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 127 | 128 | css-select@^5.1.0: 129 | version "5.1.0" 130 | resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" 131 | integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== 132 | dependencies: 133 | boolbase "^1.0.0" 134 | css-what "^6.1.0" 135 | domhandler "^5.0.2" 136 | domutils "^3.0.1" 137 | nth-check "^2.0.1" 138 | 139 | css-what@^6.1.0: 140 | version "6.1.0" 141 | resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" 142 | integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== 143 | 144 | define-data-property@^1.0.1: 145 | version "1.1.0" 146 | resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451" 147 | integrity sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g== 148 | dependencies: 149 | get-intrinsic "^1.2.1" 150 | gopd "^1.0.1" 151 | has-property-descriptors "^1.0.0" 152 | 153 | define-properties@^1.1.3: 154 | version "1.2.1" 155 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" 156 | integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== 157 | dependencies: 158 | define-data-property "^1.0.1" 159 | has-property-descriptors "^1.0.0" 160 | object-keys "^1.1.1" 161 | 162 | delay@^5.0.0: 163 | version "5.0.0" 164 | resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" 165 | integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== 166 | 167 | delayed-stream@~1.0.0: 168 | version "1.0.0" 169 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 170 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 171 | 172 | diff@^4.0.1: 173 | version "4.0.2" 174 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 175 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 176 | 177 | dom-serializer@^2.0.0: 178 | version "2.0.0" 179 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" 180 | integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== 181 | dependencies: 182 | domelementtype "^2.3.0" 183 | domhandler "^5.0.2" 184 | entities "^4.2.0" 185 | 186 | domelementtype@^2.3.0: 187 | version "2.3.0" 188 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" 189 | integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== 190 | 191 | domhandler@^5.0.2, domhandler@^5.0.3: 192 | version "5.0.3" 193 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" 194 | integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== 195 | dependencies: 196 | domelementtype "^2.3.0" 197 | 198 | domutils@^3.0.1: 199 | version "3.1.0" 200 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" 201 | integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== 202 | dependencies: 203 | dom-serializer "^2.0.0" 204 | domelementtype "^2.3.0" 205 | domhandler "^5.0.3" 206 | 207 | dotenv@^16.3.1: 208 | version "16.3.1" 209 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" 210 | integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== 211 | 212 | entities@^4.2.0: 213 | version "4.5.0" 214 | resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" 215 | integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== 216 | 217 | fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: 218 | version "3.1.3" 219 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 220 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 221 | 222 | fast-json-stringify@^5.8.0: 223 | version "5.8.0" 224 | resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-5.8.0.tgz#b229ed01ac5f92f3b82001a916c31324652f46d7" 225 | integrity sha512-VVwK8CFMSALIvt14U8AvrSzQAwN/0vaVRiFFUVlpnXSnDGrSkOAO5MtzyN8oQNjLd5AqTW5OZRgyjoNuAuR3jQ== 226 | dependencies: 227 | "@fastify/deepmerge" "^1.0.0" 228 | ajv "^8.10.0" 229 | ajv-formats "^2.1.1" 230 | fast-deep-equal "^3.1.3" 231 | fast-uri "^2.1.0" 232 | rfdc "^1.2.0" 233 | 234 | fast-printf@^1.6.9: 235 | version "1.6.9" 236 | resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676" 237 | integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg== 238 | dependencies: 239 | boolean "^3.1.4" 240 | 241 | fast-uri@^2.1.0: 242 | version "2.2.0" 243 | resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.2.0.tgz#519a0f849bef714aad10e9753d69d8f758f7445a" 244 | integrity sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg== 245 | 246 | follow-redirects@^1.15.0: 247 | version "1.15.3" 248 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" 249 | integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== 250 | 251 | form-data@^4.0.0: 252 | version "4.0.0" 253 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" 254 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 255 | dependencies: 256 | asynckit "^0.4.0" 257 | combined-stream "^1.0.8" 258 | mime-types "^2.1.12" 259 | 260 | function-bind@^1.1.1: 261 | version "1.1.1" 262 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 263 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 264 | 265 | get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1: 266 | version "1.2.1" 267 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" 268 | integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== 269 | dependencies: 270 | function-bind "^1.1.1" 271 | has "^1.0.3" 272 | has-proto "^1.0.1" 273 | has-symbols "^1.0.3" 274 | 275 | globalthis@^1.0.2: 276 | version "1.0.3" 277 | resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" 278 | integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== 279 | dependencies: 280 | define-properties "^1.1.3" 281 | 282 | gopd@^1.0.1: 283 | version "1.0.1" 284 | resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" 285 | integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== 286 | dependencies: 287 | get-intrinsic "^1.1.3" 288 | 289 | has-property-descriptors@^1.0.0: 290 | version "1.0.0" 291 | resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" 292 | integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== 293 | dependencies: 294 | get-intrinsic "^1.1.1" 295 | 296 | has-proto@^1.0.1: 297 | version "1.0.1" 298 | resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" 299 | integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== 300 | 301 | has-symbols@^1.0.3: 302 | version "1.0.3" 303 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 304 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 305 | 306 | has@^1.0.3: 307 | version "1.0.3" 308 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 309 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 310 | dependencies: 311 | function-bind "^1.1.1" 312 | 313 | he@1.2.0: 314 | version "1.2.0" 315 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 316 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 317 | 318 | http-terminator@^3.2.0: 319 | version "3.2.0" 320 | resolved "https://registry.yarnpkg.com/http-terminator/-/http-terminator-3.2.0.tgz#bc158d2694b733ca4fbf22a35065a81a609fb3e9" 321 | integrity sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g== 322 | dependencies: 323 | delay "^5.0.0" 324 | p-wait-for "^3.2.0" 325 | roarr "^7.0.4" 326 | type-fest "^2.3.3" 327 | 328 | jose@^4.14.4: 329 | version "4.14.6" 330 | resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.6.tgz#94dca1d04a0ad8c6bff0998cdb51220d473cc3af" 331 | integrity sha512-EqJPEUlZD0/CSUMubKtMaYUOtWe91tZXTWMJZoKSbLk+KtdhNdcvppH8lA9XwVu2V4Ailvsj0GBZJ2ZwDjfesQ== 332 | 333 | json-schema-traverse@^1.0.0: 334 | version "1.0.0" 335 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" 336 | integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== 337 | 338 | lru-cache@^6.0.0: 339 | version "6.0.0" 340 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 341 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 342 | dependencies: 343 | yallist "^4.0.0" 344 | 345 | make-error@^1.1.1: 346 | version "1.3.6" 347 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 348 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 349 | 350 | mime-db@1.52.0: 351 | version "1.52.0" 352 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 353 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 354 | 355 | mime-types@^2.1.12: 356 | version "2.1.35" 357 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 358 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 359 | dependencies: 360 | mime-db "1.52.0" 361 | 362 | node-html-parser@^6.1.10: 363 | version "6.1.10" 364 | resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.10.tgz#5db11eac3ccbea6fc1b04a22c8a0e3a0774cfae6" 365 | integrity sha512-6/uWdWxjQWQ7tMcFK2wWlrflsQUzh1HsEzlIf2j5+TtzfhT2yUvg3DwZYAmjEHeR3uX74ko7exjHW69J0tOzIg== 366 | dependencies: 367 | css-select "^5.1.0" 368 | he "1.2.0" 369 | 370 | nth-check@^2.0.1: 371 | version "2.1.1" 372 | resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" 373 | integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== 374 | dependencies: 375 | boolbase "^1.0.0" 376 | 377 | object-hash@^2.2.0: 378 | version "2.2.0" 379 | resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" 380 | integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== 381 | 382 | object-keys@^1.1.1: 383 | version "1.1.1" 384 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" 385 | integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== 386 | 387 | oidc-token-hash@^5.0.3: 388 | version "5.0.3" 389 | resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz#9a229f0a1ce9d4fc89bcaee5478c97a889e7b7b6" 390 | integrity sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw== 391 | 392 | openid-client@^5.5.0: 393 | version "5.5.0" 394 | resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.5.0.tgz#0c631b33c6a2c3e01197506978d6bff70e75c858" 395 | integrity sha512-Y7Xl8BgsrkzWLHkVDYuroM67hi96xITyEDSkmWaGUiNX6CkcXC3XyQGdv5aWZ6dukVKBFVQCADi9gCavOmU14w== 396 | dependencies: 397 | jose "^4.14.4" 398 | lru-cache "^6.0.0" 399 | object-hash "^2.2.0" 400 | oidc-token-hash "^5.0.3" 401 | 402 | p-finally@^1.0.0: 403 | version "1.0.0" 404 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 405 | integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== 406 | 407 | p-timeout@^3.0.0: 408 | version "3.2.0" 409 | resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" 410 | integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== 411 | dependencies: 412 | p-finally "^1.0.0" 413 | 414 | p-wait-for@^3.2.0: 415 | version "3.2.0" 416 | resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f" 417 | integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA== 418 | dependencies: 419 | p-timeout "^3.0.0" 420 | 421 | proxy-from-env@^1.1.0: 422 | version "1.1.0" 423 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 424 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 425 | 426 | punycode@^2.1.0: 427 | version "2.3.0" 428 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" 429 | integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== 430 | 431 | require-from-string@^2.0.2: 432 | version "2.0.2" 433 | resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" 434 | integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== 435 | 436 | rfdc@^1.2.0: 437 | version "1.3.0" 438 | resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" 439 | integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== 440 | 441 | roarr@^7.0.4: 442 | version "7.15.1" 443 | resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.1.tgz#e4d93105c37b5ea7dd1200d96a3500f757ddc39f" 444 | integrity sha512-0ExL9rjOXeQPvQvQo8IcV8SR2GTXmDr1FQFlY2HiAV+gdVQjaVZNOx9d4FI2RqFFsd0sNsiw2TRS/8RU9g0ZfA== 445 | dependencies: 446 | boolean "^3.1.4" 447 | fast-json-stringify "^5.8.0" 448 | fast-printf "^1.6.9" 449 | globalthis "^1.0.2" 450 | safe-stable-stringify "^2.4.3" 451 | semver-compare "^1.0.0" 452 | 453 | safe-stable-stringify@^2.4.3: 454 | version "2.4.3" 455 | resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" 456 | integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== 457 | 458 | semver-compare@^1.0.0: 459 | version "1.0.0" 460 | resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" 461 | integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== 462 | 463 | set-cookie-parser@^2.6.0: 464 | version "2.6.0" 465 | resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" 466 | integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== 467 | 468 | ts-node@^10.9.1: 469 | version "10.9.1" 470 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" 471 | integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== 472 | dependencies: 473 | "@cspotcode/source-map-support" "^0.8.0" 474 | "@tsconfig/node10" "^1.0.7" 475 | "@tsconfig/node12" "^1.0.7" 476 | "@tsconfig/node14" "^1.0.0" 477 | "@tsconfig/node16" "^1.0.2" 478 | acorn "^8.4.1" 479 | acorn-walk "^8.1.1" 480 | arg "^4.1.0" 481 | create-require "^1.1.0" 482 | diff "^4.0.1" 483 | make-error "^1.1.1" 484 | v8-compile-cache-lib "^3.0.1" 485 | yn "3.1.1" 486 | 487 | type-fest@^2.3.3: 488 | version "2.19.0" 489 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" 490 | integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== 491 | 492 | typescript@^5.2.2: 493 | version "5.2.2" 494 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" 495 | integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== 496 | 497 | uri-js@^4.2.2: 498 | version "4.4.1" 499 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" 500 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== 501 | dependencies: 502 | punycode "^2.1.0" 503 | 504 | v8-compile-cache-lib@^3.0.1: 505 | version "3.0.1" 506 | resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" 507 | integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== 508 | 509 | yallist@^4.0.0: 510 | version "4.0.0" 511 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 512 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 513 | 514 | yn@3.1.1: 515 | version "3.1.1" 516 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 517 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 518 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 |