diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..e29b8969
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,20 @@
+.editorconfig export-ignore
+.gitattributes export-ignore
+.github/ export-ignore
+.gitignore export-ignore
+.php-cs-fixer.dist.php export-ignore
+.semgrepignore export-ignore
+.shiprc export-ignore
+CHANGELOG.ARCHIVE.md export-ignore
+CHANGELOG.md export-ignore
+docs/ export-ignore
+EXAMPLES.md export-ignore
+examples/ export-ignore
+opslevel.yml export-ignore
+phpstan.neon.dist export-ignore
+phpunit.xml.dist export-ignore
+psalm.xml.dist export-ignore
+rector.php export-ignore
+tests/ export-ignore
+UPGRADE.md export-ignore
+vendor/ export-ignore
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..d04a2291
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,3 @@
+# Code of Conduct
+
+Before making any contributions to this repo, please review Auth0's [Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). By contributing, you agree to uphold this code.
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 00000000..55056aa9
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,93 @@
+# Contribution Guide
+
+- [Getting Involved](#getting-involved)
+- [Support Questions](#support-questions)
+- [Code Contributions](#code-contributions)
+- [Security Vulnerabilities](#security-vulnerabilities)
+- [Coding Style](#coding-style)
+ - [PHPDoc](#phpdoc)
+- [Code of Conduct](#code-of-conduct)
+
+## Getting Involved
+
+To encourage active collaboration, Auth0 strongly encourages pull requests, not just bug reports. Pull requests will only be reviewed when marked as "ready for review" (not in the "draft" state) and all tests for new features are passing. Lingering, non-active pull requests left in the "draft" state will eventually be closed.
+
+If you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a bug report is to make it easy for yourself - and others - to replicate the bug and develop a fix.
+
+Remember, bug reports are created in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the bug report will automatically see any activity or that others will jump to fix it. Creating a bug report serves to help you and others start on the path of fixing the problem. If you want to chip in, you can help out by fixing any bugs listed in our issue trackers.
+
+## Support Questions
+
+Auth0's GitHub issue trackers are not intended to provide integration support. Instead, please refer your questions to the [Auth0 Community](https://community.auth0.com).
+
+## Code Contributions
+
+You may propose new features or improvements to existing SDK behavior by creating a feature request within the repository's issue tracker. If you are willing to implement at least some of the code that would be needed to complete the feature, please fork the repository and submit a pull request.
+
+All development should be done in individual forks using dedicated branches, and submitted against the `main` default branch.
+
+Pull request titles must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) rules so our changelogs can be automatically generated. Commits messages are irrelevant as they will be squashed into the Pull request's title during a merge.
+
+The following types are allowed:
+
+- _feat:_ A new feature
+- _perf:_ A code change that improves performance
+- _refactor:_ A code change that neither fixes a bug nor adds a feature
+- _build:_ Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
+- _ci:_ Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
+- _style:_ Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
+- _fix:_ A bug fix
+- _security:_ A change that improves security
+- _docs:_ Documentation only changes
+- _test:_ Adding missing tests or correcting existing tests
+
+## Security Vulnerabilities
+
+If you discover a security vulnerability within this SDK, please review Auth0's [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. All security vulnerabilities will be promptly addressed.
+
+## Unit Testing and 100% Minimum Coverage
+
+We use [PEST](https://pestphp.com/) for testing. You can run `composer pest` to run the test suite. You can also run `composer pest:coverage` to generate a code coverage report.
+
+We require 100% code coverage for all new features. If you are adding a new feature, please add tests to cover all of the new code. If you are fixing a bug, please add a test that reproduces the bug and then shows that it has been fixed.
+
+Pull requests that do not meet the minimum coverage requirements will not be merged.
+
+## Static Analysis
+
+We use [PHPStan](https://phpstan.org) and [Psalm](https://psalm.dev/) for static analysis. You can use `composer phpstan` and `composer psalm` to run them.
+
+## Coding Style
+
+We use [PHP CS Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) to ensure that code styling is consistent. You can run `composer phpcs` to check for any code style issues. `composer phpcs:fix` will attempt to automatically fix the issues, but be cautious as it may not always get it right.
+
+We also use [Rector](https://github.com/rectorphp/rector) to catch edge cases where more optimal refactoring can be made. You can run `composer rector` to check for any recommendations, and `composer rector:fix` to accept the suggestions.
+
+It's important to note that our GitHub CI will also run these checks for pull requests, but you should run these locally first to avoid any surprises when you push your code. If you disagree with one of these recommendations, please bring it up in the pull request so we can discuss it. We may decide to adjust the styling rules if we feel it's warranted, but we prefer to avoid it if possible.
+
+### PHPDoc
+
+All public methods and classes should be documented with PHPDoc blocks.
+
+Below is an example of a valid documentation block. Note that the @param attribute is followed by two spaces, the argument type, two more spaces, and finally the variable name:
+
+```php
+/**
+ * Register a binding with the container.
+ *
+ * @param string|array $abstract
+ * @param \Closure|string|null $concrete
+ * @param bool $shared
+ * @return void
+ *
+ * @throws \Exception
+ */
+public function bind($abstract, $concrete = null, $shared = false)
+{
+ //
+}
+```
+
+## Code of Conduct
+
+Before making any contributions to this repo, please review Auth0's [Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). By contributing, you agree to uphold this code.
diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yml b/.github/ISSUE_TEMPLATE/Bug Report.yml
deleted file mode 100644
index 32252601..00000000
--- a/.github/ISSUE_TEMPLATE/Bug Report.yml
+++ /dev/null
@@ -1,62 +0,0 @@
-name: Report a Bug
-description: Found a bug or issue? Let us know with a bug report.
-labels: [triage]
-body:
- - type: markdown
- attributes:
- value: Thanks for taking the time to help us improve this project!
- - type: dropdown
- id: sdk
- attributes:
- label: SDK Version
- description: What version of our Laravel plugin are you running? (`composer show | grep auth0/login`)
- options:
- - 7.2
- - 7.1
- - 7.0
- - 6.5
- - 6.4
- - Other (specify in 'additional context')
- validations:
- required: true
- - type: dropdown
- id: php
- attributes:
- label: PHP Version
- description: What version of PHP are you running? (`php -v`)
- options:
- - PHP 8.2
- - PHP 8.1
- - PHP 8.0
- - Other (specify in 'additional context')
- validations:
- required: true
- - type: dropdown
- id: composer
- attributes:
- label: Composer Version
- description: What version of Composer are you running? (`composer -v`)
- options:
- - 2.x
- - 1.x
- validations:
- required: true
- - type: textarea
- id: bug-description
- attributes:
- label: What happened?
- description: Also tell us, what did you expect to happen?
- validations:
- required: true
- - type: textarea
- id: bug-reproduction
- attributes:
- label: How can we reproduce this issue?
- description: Detail the steps taken to reproduce this error, and whether this can be reproduced consistently or if it is intermittent.
- validations:
- required: true
- - type: textarea
- id: context
- attributes:
- label: Additional context
- description: Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/Feature Request.yml b/.github/ISSUE_TEMPLATE/Feature Request.yml
deleted file mode 100644
index ecf3e049..00000000
--- a/.github/ISSUE_TEMPLATE/Feature Request.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: Suggest a Feature
-description: Suggest an idea or a feature for this project.
-labels: [triage]
-body:
- - type: markdown
- attributes:
- value: Thanks for taking the time to help us improve this project!
- - type: textarea
- id: feature-description
- attributes:
- label: What should be added?
- description: How do you think this feature will improve the developer experience?
- validations:
- required: true
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..d85577fa
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,90 @@
+name: Report a Bug
+description: Encountering unexpected problems or unintended behavior? Let us know!
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Please do not report security vulnerabilities here**. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues.
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: This can be reproduced using [the quickstart sample application](https://github.com/auth0-samples/laravel).
+ required: true
+ - label: I have looked at [the README](https://github.com/auth0/laravel-auth0/#readme) and have not found a solution.
+ required: true
+ - label: I have looked at [the `docs` directory](https://github.com/auth0/laravel-auth0/blob/main/docs) and have not found a solution.
+ required: true
+ - label: I have searched [previous issues](https://github.com/auth0/laravel-auth0/issues) and have not found a solution.
+ required: true
+ - label: I have searched [the Auth0 Community](https://community.auth0.com/tag/laravel) and have not found a solution.
+ required: true
+ - label: I agree to uphold [the Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).
+ required: true
+
+ - type: dropdown
+ id: laravel
+ attributes:
+ label: Laravel Version
+ description: What version of Laravel are you using? (`composer show | grep laravel/framework`)
+ options:
+ - 10
+ - 9
+ - Other (specify below)
+ validations:
+ required: true
+
+ - type: dropdown
+ id: sdk
+ attributes:
+ label: SDK Version
+ description: What version of our SDK are you using? (`composer show | grep auth0/login`)
+ options:
+ - 7.13
+ - 7.12
+ - 7.11
+ - 7.10
+ - 7.9
+ - 7.8
+ - 7.7
+ - 7.6
+ - 7.5
+ - 7.4
+ - 7.3
+ - 7.2
+ - 7.1
+ - 7.0
+ - Other (specify below)
+ validations:
+ required: true
+
+ - type: dropdown
+ id: php
+ attributes:
+ label: PHP Version
+ description: What version of PHP are you running? (`php -v`)
+ options:
+ - PHP 8.3
+ - PHP 8.2
+ - Other (specify below)
+ validations:
+ required: true
+
+ - type: textarea
+ id: bug-description
+ attributes:
+ label: Description
+ description: Provide a description of the issue, including what you expected to happen.
+ validations:
+ required: true
+
+ - type: textarea
+ id: bug-reproduction
+ attributes:
+ label: How can we reproduce this issue?
+ description: Detail the steps taken to reproduce this error. If possible, please provide a GitHub repository to demonstrate the issue.
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index b921445c..18c8450a 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -4,7 +4,7 @@ contact_links:
url: https://github.com/auth0/auth0-PHP/
about: For issues relating to the Auth0-PHP SDK, please report them in that repository.
- name: Community Support
- url: https://community.auth0.com/tags/c/sdks/5/laravel
+ url: https://community.auth0.com/tag/laravel
about: Please ask general usage questions here.
- name: Responsible Disclosure Program
url: https://auth0.com/whitehat
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 00000000..7bab51f8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,23 @@
+name: Suggest a Feature
+description: Help us improve the SDK by suggest new features and improvements.
+
+body:
+ - type: markdown
+ attributes:
+ value: Thanks for taking the time to help us improve this SDK!
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: I agree to uphold [the Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).
+ required: true
+
+ - type: textarea
+ id: feature-description
+ attributes:
+ label: Description
+ description: Please provide a summary of the change you'd like considered, including any relevant context.
+ validations:
+ required: true
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index fc9f6f7a..8837cb85 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,35 +1,28 @@
### Changes
### References
-Resolves #
-
### Testing
### Contributor Checklist
-- [ ] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
-- [ ] I have read the [Auth0 code of conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
+- [ ] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
+- [ ] I have read the [Auth0 code of conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 00000000..0738ecdf
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,11 @@
+# Security Policy
+
+**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
+
+## Supported Versions
+
+Please see [our support policy](https://github.com/auth0/laravel-auth0#requirements) for information on supported versions for security releases.
+
+## Reporting a Vulnerability
+
+If you discover a security vulnerability within this SDK, please review Auth0's [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. All security vulnerabilities will be promptly addressed.
diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md
new file mode 100644
index 00000000..dbd8a8c8
--- /dev/null
+++ b/.github/SUPPORT.md
@@ -0,0 +1,3 @@
+# Support Questions
+
+Auth0's GitHub issue trackers are not intended to provide integration support. Instead, please refer your questions to the [Auth0 Community](https://community.auth0.com).
diff --git a/.github/actions/get-prerelease/action.yml b/.github/actions/get-prerelease/action.yml
new file mode 100644
index 00000000..ce7acdc3
--- /dev/null
+++ b/.github/actions/get-prerelease/action.yml
@@ -0,0 +1,30 @@
+name: Return a boolean indicating if the version contains prerelease identifiers
+
+#
+# Returns a simple true/false boolean indicating whether the version indicates it's a prerelease or not.
+#
+# TODO: Remove once the common repo is public.
+#
+
+inputs:
+ version:
+ required: true
+
+outputs:
+ prerelease:
+ value: ${{ steps.get_prerelease.outputs.PRERELEASE }}
+
+runs:
+ using: composite
+
+ steps:
+ - id: get_prerelease
+ shell: bash
+ run: |
+ if [[ "${VERSION}" == *"beta"* || "${VERSION}" == *"alpha"* ]]; then
+ echo "PRERELEASE=true" >> $GITHUB_OUTPUT
+ else
+ echo "PRERELEASE=false" >> $GITHUB_OUTPUT
+ fi
+ env:
+ VERSION: ${{ inputs.version }}
diff --git a/.github/actions/get-version/action.yml b/.github/actions/get-version/action.yml
new file mode 100644
index 00000000..387fdba6
--- /dev/null
+++ b/.github/actions/get-version/action.yml
@@ -0,0 +1,23 @@
+name: Return the version extracted from the branch name
+
+#
+# Returns the version from a branch name of a pull request. It expects the branch name to be in the format release/vX.Y.Z, release/X.Y.Z, release/vX.Y.Z-beta.N. etc.
+#
+# TODO: Remove once the common repo is public.
+#
+
+outputs:
+ version:
+ value: ${{ steps.get_version.outputs.VERSION }}
+
+runs:
+ using: composite
+
+ steps:
+ - id: get_version
+ shell: bash
+ run: |
+ VERSION=$(echo ${BRANCH_NAME} | sed -r 's#release/+##g')
+ echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
+ env:
+ BRANCH_NAME: ${{ github.event.pull_request.head.ref }}
diff --git a/.github/actions/publish-package/action.yml b/.github/actions/publish-package/action.yml
new file mode 100644
index 00000000..d38b3e49
--- /dev/null
+++ b/.github/actions/publish-package/action.yml
@@ -0,0 +1,29 @@
+name: Publish release to package manager
+
+inputs:
+ token:
+ required: true
+ files:
+ required: false
+ name:
+ required: true
+ body:
+ required: true
+ tag:
+ required: true
+ commit:
+ required: true
+ draft:
+ default: false
+ required: false
+ prerelease:
+ default: false
+ required: false
+
+runs:
+ using: composite
+
+ steps:
+ # Nothing to do for PHP.
+ - run: exit 0
+ shell: bash
diff --git a/.github/actions/release-create/action.yml b/.github/actions/release-create/action.yml
new file mode 100644
index 00000000..6a2bf804
--- /dev/null
+++ b/.github/actions/release-create/action.yml
@@ -0,0 +1,47 @@
+name: Create a GitHub release
+
+#
+# Creates a GitHub release with the given version.
+#
+# TODO: Remove once the common repo is public.
+#
+
+inputs:
+ token:
+ required: true
+ files:
+ required: false
+ name:
+ required: true
+ body:
+ required: true
+ tag:
+ required: true
+ commit:
+ required: true
+ draft:
+ default: false
+ required: false
+ prerelease:
+ default: false
+ required: false
+ fail_on_unmatched_files:
+ default: true
+ required: false
+
+runs:
+ using: composite
+
+ steps:
+ - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844
+ with:
+ body: ${{ inputs.body }}
+ name: ${{ inputs.name }}
+ tag_name: ${{ inputs.tag }}
+ target_commitish: ${{ inputs.commit }}
+ draft: ${{ inputs.draft }}
+ prerelease: ${{ inputs.prerelease }}
+ fail_on_unmatched_files: ${{ inputs.fail_on_unmatched_files }}
+ files: ${{ inputs.files }}
+ env:
+ GITHUB_TOKEN: ${{ inputs.token }}
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
new file mode 100644
index 00000000..31ad37c5
--- /dev/null
+++ b/.github/actions/setup/action.yml
@@ -0,0 +1,48 @@
+name: Prepare PHP
+description: Prepare the PHP environment
+
+inputs:
+ php:
+ description: The PHP version to use
+ required: true
+ coverage:
+ description: The coverage extension to use
+ required: false
+ default: 'none'
+ extensions:
+ description: The PHP extensions to use
+ required: false
+ default: 'none, mbstring, curl, simplexml, dom, xmlwriter, xml, tokenizer, fileinfo, pdo'
+ runner:
+ description: The runner OS
+ required: false
+ default: 'ubuntu-latest'
+
+runs:
+ using: composite
+
+ steps:
+ - name: Setup PHP
+ uses: shivammathur/setup-php@4bd44f22a98a19e0950cbad5f31095157cc9621b # pin@2.25.4
+ with:
+ php-version: ${{ inputs.php }}
+ extensions: ${{ inputs.extensions }}
+ coverage: ${{ inputs.coverage }}
+ env:
+ runner: ${{ inputs.runner }}
+
+ - name: Get Composer cache directory
+ id: composer-cache
+ shell: bash
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ inputs.php }}-${{ hashFiles('**/composer.lock') }}
+ restore-keys: ${{ runner.os }}-composer-${{ inputs.php }}-
+
+ - name: Install dependencies
+ shell: bash
+ run: composer install --prefer-dist
diff --git a/.github/actions/tag-create/action.yml b/.github/actions/tag-create/action.yml
new file mode 100644
index 00000000..727df485
--- /dev/null
+++ b/.github/actions/tag-create/action.yml
@@ -0,0 +1,33 @@
+name: Create a repository tag
+
+#
+# Creates a tag with the given version.
+#
+# TODO: Remove once the common repo is public.
+#
+
+inputs:
+ token:
+ required: true
+ tag:
+ required: true
+
+runs:
+ using: composite
+
+ steps:
+ - shell: bash
+ run: |
+ git config user.name "${AUTHOR_USERNAME}"
+ git config user.email "${AUTHOR_EMAIL}"
+ env:
+ AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }}
+ AUTHOR_EMAIL: ${{ github.event.pull_request.user.email }}
+
+ - shell: bash
+ run: |
+ git tag -a ${TAG_NAME} -m "Version ${TAG_NAME}"
+ git push --follow-tags
+ env:
+ TAG_NAME: ${{ inputs.tag }}
+ GITHUB_TOKEN: ${{ inputs.token }}
diff --git a/.github/actions/tag-exists/action.yml b/.github/actions/tag-exists/action.yml
new file mode 100644
index 00000000..b5fbdb73
--- /dev/null
+++ b/.github/actions/tag-exists/action.yml
@@ -0,0 +1,36 @@
+name: Return a boolean indicating if a tag already exists for the repository
+
+#
+# Returns a simple true/false boolean indicating whether the tag exists or not.
+#
+# TODO: Remove once the common repo is public.
+#
+
+inputs:
+ token:
+ required: true
+ tag:
+ required: true
+
+outputs:
+ exists:
+ description: 'Whether the tag exists or not'
+ value: ${{ steps.tag-exists.outputs.EXISTS }}
+
+runs:
+ using: composite
+
+ steps:
+ - id: tag-exists
+ shell: bash
+ run: |
+ GET_API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG_NAME}"
+ http_status_code=$(curl -LI $GET_API_URL -o /dev/null -w '%{http_code}\n' -s -H "Authorization: token ${GITHUB_TOKEN}")
+ if [ "$http_status_code" -ne "404" ] ; then
+ echo "EXISTS=true" >> $GITHUB_OUTPUT
+ else
+ echo "EXISTS=false" >> $GITHUB_OUTPUT
+ fi
+ env:
+ TAG_NAME: ${{ inputs.tag }}
+ GITHUB_TOKEN: ${{ inputs.token }}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index dfb465a1..12301490 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,7 +1,6 @@
version: 2
updates:
- - package-ecosystem: composer
+ - package-ecosystem: "github-actions"
directory: "/"
schedule:
- interval: daily
- open-pull-requests-limit: 10
+ interval: "daily"
diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index bf9b1cf4..00000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-# Configuration for probot-stale - https://github.com/probot/stale
-
-# Number of days of inactivity before an Issue or Pull Request becomes stale
-daysUntilStale: 120
-
-# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
-daysUntilClose: 21
-
-# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
-onlyLabels:
- - 'Waiting for Response'
-
-# Set to true to ignore issues with an assignee (defaults to false)
-exemptAssignees: true
-
-# Label to use when marking as stale
-staleLabel: closed:stale
-
-# Comment to post when marking as stale. Set to `false` to disable
-markComment: >
- This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇♂️
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
deleted file mode 100644
index 2d965938..00000000
--- a/.github/workflows/checks.yml
+++ /dev/null
@@ -1,301 +0,0 @@
-name: Checks
-
-on:
- push:
- branches:
- - main
- pull_request:
-
-jobs:
- dependencies:
- name: Dependencies
- runs-on: ubuntu-latest
-
- strategy:
- max-parallel: 10
- matrix:
- php: ["8.0", "8.1", "8.2"]
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Set up PHP ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- coverage: none
- extensions: mbstring
- env:
- update: true
- COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Get composer cache directory
- id: composer-cache
- run: echo "::set-output name=dir::$(composer config cache-files-dir)"
-
- - name: Cache dependencies
- uses: actions/cache@v2
- with:
- path: ${{ steps.composer-cache.outputs.dir }}
- key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
-
- - name: Install dependencies with composer
- run: composer update --no-ansi --no-interaction --no-progress --prefer-dist --prefer-stable
-
- normalize:
- name: Normalize
- runs-on: ubuntu-latest
- needs: ["dependencies"]
-
- strategy:
- fail-fast: true
- max-parallel: 10
- matrix:
- php: ["8.0"]
-
- steps:
- - name: Set up PHP ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- coverage: none
- extensions: mbstring
- env:
- update: true
- COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Get composer cache directory
- id: composer-cache
- run: echo "::set-output name=dir::$(composer config cache-files-dir)"
-
- - name: Cache dependencies
- uses: actions/cache@v2
- with:
- path: ${{ steps.composer-cache.outputs.dir }}
- key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
- restore-keys: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
-
- - name: Install dependencies
- run: composer install --prefer-dist
-
- - name: Execute Normalize
- run: composer normalize
-
- pest:
- name: Pest
- runs-on: ubuntu-latest
- continue-on-error: true
- needs: ["dependencies"]
-
- strategy:
- max-parallel: 10
- matrix:
- php: ["8.0", "8.1", "8.2"]
-
- steps:
- - name: Set up PHP ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- coverage: pcov
- extensions: mbstring
- env:
- update: true
- COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Get composer cache directory
- id: composer-cache
- run: echo "::set-output name=dir::$(composer config cache-files-dir)"
-
- - name: Cache dependencies
- uses: actions/cache@v2
- with:
- path: ${{ steps.composer-cache.outputs.dir }}
- key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
- restore-keys: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
-
- - name: Install dependencies with composer
- run: composer install --prefer-dist
-
- - name: Execute Pest
- run: vendor/bin/pest --coverage-clover coverage/coverage.xml --no-interaction
-
- - if: (matrix.php == '8.1')
- uses: codecov/codecov-action@v2
- with:
- directory: coverage
-
- phpstan:
- name: PHPStan
- runs-on: ubuntu-latest
- needs: ["dependencies"]
-
- strategy:
- fail-fast: true
- max-parallel: 10
- matrix:
- php: ["8.0", "8.1", "8.2"]
-
- steps:
- - name: Set up PHP ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- coverage: none
- extensions: mbstring
- env:
- update: true
- COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Get composer cache directory
- id: composer-cache
- run: echo "::set-output name=dir::$(composer config cache-files-dir)"
-
- - name: Cache dependencies
- uses: actions/cache@v2
- with:
- path: ${{ steps.composer-cache.outputs.dir }}
- key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
- restore-keys: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
-
- - name: Install dependencies
- run: composer install --prefer-dist
-
- - name: Execute PHPStan
- run: vendor/bin/phpstan analyze --no-progress
-
- psalm:
- name: Psalm
- runs-on: ubuntu-latest
- needs: ["dependencies"]
-
- strategy:
- fail-fast: true
- max-parallel: 10
- matrix:
- php: ["8.0", "8.1", "8.2"]
-
- steps:
- - name: Set up PHP ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- coverage: none
- extensions: mbstring
- env:
- update: true
- COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Get composer cache directory
- id: composer-cache
- run: echo "::set-output name=dir::$(composer config cache-files-dir)"
-
- - name: Cache dependencies
- uses: actions/cache@v2
- with:
- path: ${{ steps.composer-cache.outputs.dir }}
- key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
- restore-keys: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
-
- - name: Install dependencies
- run: composer install --prefer-dist
-
- - name: Execute Psalm
- run: vendor/bin/psalm --no-progress
-
- pint:
- name: Laravel Pint
- runs-on: ubuntu-latest
- needs: ["dependencies"]
-
- strategy:
- fail-fast: true
- max-parallel: 10
- matrix:
- php: ["8.0", "8.1", "8.2"]
-
- steps:
- - name: Set up PHP ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- coverage: none
- extensions: mbstring
- env:
- update: true
- COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Get composer cache directory
- id: composer-cache
- run: echo "::set-output name=dir::$(composer config cache-files-dir)"
-
- - name: Cache dependencies
- uses: actions/cache@v2
- with:
- path: ${{ steps.composer-cache.outputs.dir }}
- key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
- restore-keys: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
-
- - name: Install dependencies
- run: composer install --prefer-dist
-
- - name: Execute Laravel Pint
- run: vendor/bin/pint --test
-
- rector:
- name: Rector
- runs-on: ubuntu-latest
- needs: ["dependencies"]
-
- strategy:
- fail-fast: true
- max-parallel: 10
- matrix:
- php: ["8.0", "8.1", "8.2"]
-
- steps:
- - name: Set up PHP ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- coverage: none
- extensions: mbstring
- env:
- update: true
- COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Get composer cache directory
- id: composer-cache
- run: echo "::set-output name=dir::$(composer config cache-files-dir)"
-
- - name: Cache dependencies
- uses: actions/cache@v2
- with:
- path: ${{ steps.composer-cache.outputs.dir }}
- key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
- restore-keys: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}-${{ github.run_id }}
-
- - name: Install dependencies
- run: composer install --prefer-dist
-
- - name: Execute Rector
- run: vendor/bin/rector process src --dry-run
diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml
deleted file mode 100644
index a862d87a..00000000
--- a/.github/workflows/maintenance.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: 'Repository Maintenance'
-
-on:
- schedule:
- - cron: '0 0 * * *'
- workflow_dispatch:
-
-permissions:
- issues: write
- pull-requests: write
-
-concurrency:
- group: lock
-
-jobs:
- action:
- runs-on: ubuntu-latest
- steps:
- - uses: dessant/lock-threads@v3
- with:
- issue-inactive-days: '30'
- pr-inactive-days: '30'
diff --git a/.github/workflows/matrix.json b/.github/workflows/matrix.json
new file mode 100644
index 00000000..68634def
--- /dev/null
+++ b/.github/workflows/matrix.json
@@ -0,0 +1,6 @@
+{
+ "include": [
+ { "php": "8.2" },
+ { "php": "8.3" }
+ ]
+}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..7aa14e53
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,70 @@
+name: Create GitHub Release
+
+on:
+ pull_request:
+ types:
+ - closed
+
+permissions:
+ contents: write
+
+### TODO: Replace instances of './.github/actions/' w/ `auth0/dx-sdk-actions/` and append `@latest` after the common `dx-sdk-actions` repo is made public.
+### TODO: Also remove `get-prerelease`, `get-version`, `release-create`, `tag-create` and `tag-exists` actions from this repo's .github/actions folder once the repo is public.
+
+jobs:
+ release:
+ if: github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')
+ runs-on: ubuntu-latest
+
+ steps:
+ # Checkout the code
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ # Get the version from the branch name
+ - id: get_version
+ uses: ./.github/actions/get-version
+
+ # Get the prerelease flag from the branch name
+ - id: get_prerelease
+ uses: ./.github/actions/get-prerelease
+ with:
+ version: ${{ steps.get_version.outputs.version }}
+
+ # Check if the tag already exists
+ - id: tag_exists
+ uses: ./.github/actions/tag-exists
+ with:
+ tag: ${{ steps.get_version.outputs.version }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ # If the tag already exists, exit with an error
+ - if: steps.tag_exists.outputs.exists == 'true'
+ run: exit 1
+
+ # Publish the release to our package manager
+ - uses: ./.github/actions/publish-package
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ name: ${{ steps.get_version.outputs.version }}
+ body: ${{ github.event.pull_request.body }}
+ tag: ${{ steps.get_version.outputs.version }}
+ commit: ${{ github.sha }}
+ prerelease: ${{ steps.get_prerelease.outputs.prerelease }}
+
+ # Create a tag for the release
+ - uses: ./.github/actions/tag-create
+ with:
+ tag: ${{ steps.get_version.outputs.version }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ # Create a release for the tag
+ - uses: ./.github/actions/release-create
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ name: ${{ steps.get_version.outputs.version }}
+ body: ${{ github.event.pull_request.body }}
+ tag: ${{ steps.get_version.outputs.version }}
+ commit: ${{ github.sha }}
+ prerelease: ${{ steps.get_prerelease.outputs.prerelease }}
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
deleted file mode 100644
index 10fd3096..00000000
--- a/.github/workflows/security.yml
+++ /dev/null
@@ -1,84 +0,0 @@
-name: Security
-
-on:
- push:
- branches:
- - main
- pull_request:
-
-jobs:
- snyk:
- name: Snyk
- runs-on: ubuntu-latest
- strategy:
- max-parallel: 10
- matrix:
- php: ["8.0", "8.1", "8.2"]
-
- if: (github.actor != 'dependabot[bot]')
- steps:
- - name: Set up PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- coverage: none
- extensions: mbstring
-
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Install dependencies
- run: composer update --no-interaction --no-progress
-
- - name: Run Snyk to check for vulnerabilities
- uses: snyk/actions/php@master
- continue-on-error: true
- env:
- SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- with:
- args: --severity-threshold=high --sarif-file-output=snyk.sarif
-
- - name: Check to see if the SARIF a was generated
- id: sarif_file_exists
- uses: andstor/file-existence-action@v1
- with:
- files: "snyk.sarif"
-
- - name: Upload result to GitHub Code Scanning
- uses: github/codeql-action/upload-sarif@v2
- if: steps.sarif_file_exists.outputs.files_exists == 'true'
- with:
- sarif_file: snyk.sarif
-
- semgrep:
- name: Semgrep
- runs-on: ubuntu-latest
- container:
- image: returntocorp/semgrep
-
- if: (github.actor != 'dependabot[bot]')
- steps:
- - uses: actions/checkout@v3
-
- - run: semgrep scan --sarif --output=semgrep.sarif
- env:
- SEMGREP_RULES: >-
- p/phpcs-security-audit
- p/security-audit
- p/secrets
- p/owasp-top-ten
- SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
- SEMGREP_REPO_NAME: "auth0/laravel-auth0"
- SEMGREP_REPO_URL: "https://github.com/auth0/laravel-auth0"
-
- - name: Check to see if the SARIF a was generated
- id: sarif_file_exists
- uses: andstor/file-existence-action@v1
- with:
- files: "semgrep.sarif"
-
- - name: Upload SARIF file for GitHub Advanced Security Dashboard
- uses: github/codeql-action/upload-sarif@v2
- with:
- sarif_file: semgrep.sarif
- if: always()
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
new file mode 100644
index 00000000..e53b2034
--- /dev/null
+++ b/.github/workflows/semgrep.yml
@@ -0,0 +1,49 @@
+name: Semgrep
+
+on:
+ merge_group:
+ pull_request_target:
+ types:
+ - opened
+ - synchronize
+ push:
+ branches:
+ - main
+ schedule:
+ - cron: "30 0 1,15 * *"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+ authorize:
+ name: Authorize
+ environment: ${{ github.actor != 'dependabot[bot]' && github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && 'external' || 'internal' }}
+ runs-on: ubuntu-latest
+ steps:
+ - run: true
+
+ check:
+ needs: authorize
+
+ name: Check for Vulnerabilities
+ runs-on: ubuntu-latest
+
+ container:
+ image: returntocorp/semgrep
+
+ steps:
+ - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group'
+ run: exit 0
+
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.merge_commit_sha || github.ref }}
+
+ - run: semgrep ci
+ env:
+ SEMGREP_APP_TOKEN: ${{ secrets.DX_SDKS_SEMGREP_TOKEN }}
diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml
new file mode 100644
index 00000000..516645a8
--- /dev/null
+++ b/.github/workflows/snyk.yml
@@ -0,0 +1,88 @@
+name: Snyk
+
+on:
+ merge_group:
+ pull_request_target:
+ types:
+ - opened
+ - synchronize
+ push:
+ branches:
+ - main
+ schedule:
+ - cron: "30 0 1,15 * *"
+
+permissions:
+ contents: read
+
+env:
+ DX_SDKS_SNYK_ORGANIZATION: 8303ea71-ac72-4ae6-9cd0-ae2f3eda82b7
+ DX_SDKS_SNYK_PROJECT: auth0/laravel-auth0
+ DX_SDKS_SNYK_TAGS: Refactoring-target:DX,Refactoring-origin:auth0-sdks
+ DX_SDKS_SNYK_REMOTE_REPO_URL: https://github.com/auth0/laravel-auth0
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+ authorize:
+ name: Authorize
+ environment: ${{ github.actor != 'dependabot[bot]' && github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && 'external' || 'internal' }}
+ runs-on: ubuntu-latest
+ steps:
+ - run: true
+
+ configure:
+ name: Configure
+ needs: [authorize]
+ runs-on: ubuntu-latest
+
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.merge_commit_sha || github.ref }}
+
+ - id: set-matrix
+ run: echo "matrix=$(jq -c . < ./.github/workflows/matrix.json)" >> $GITHUB_OUTPUT
+
+ check:
+ needs: [configure]
+
+ name: Check for Vulnerabilities
+ runs-on: ubuntu-latest
+
+ steps:
+ - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group'
+ run: exit 0
+
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.merge_commit_sha || github.ref }}
+
+ - uses: ./.github/actions/setup
+ with:
+ php: ${{ fromJson(needs.configure.outputs.matrix).include[0].php }}
+
+ - run: npm install snyk -g
+
+ - if: github.ref == 'refs/heads/main'
+ run: snyk monitor --file=composer.lock --org=$SNYK_ORGANIZATION --project-name=$SNYK_PROJECT --project-tags=$SNYK_TAGS --remote-repo-url=$SNYK_REMOTE_REPO --target-reference="$(git branch --show-current)"
+ env:
+ SNYK_TOKEN: ${{ secrets.DX_SDKS_SNYK_TOKEN }}
+ SNYK_ORGANIZATION: ${{ env.DX_SDKS_SNYK_ORGANIZATION }}
+ SNYK_PROJECT: ${{ env.DX_SDKS_SNYK_PROJECT }}
+ SNYK_TAGS: ${{ env.DX_SDKS_SNYK_TAGS }}
+ SNYK_REMOTE_REPO: ${{ env.DX_SDKS_SNYK_REMOTE_REPO_URL }}
+ continue-on-error: true
+
+ - run: snyk test --file=composer.lock --org=$SNYK_ORGANIZATION --project-name=$SNYK_PROJECT --remote-repo-url=$SNYK_REMOTE_REPO
+ env:
+ SNYK_TOKEN: ${{ secrets.DX_SDKS_SNYK_TOKEN }}
+ SNYK_ORGANIZATION: ${{ env.DX_SDKS_SNYK_ORGANIZATION }}
+ SNYK_PROJECT: ${{ env.DX_SDKS_SNYK_PROJECT }}
+ SNYK_TAGS: ${{ env.DX_SDKS_SNYK_TAGS }}
+ SNYK_REMOTE_REPO: ${{ env.DX_SDKS_SNYK_REMOTE_REPO_URL }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 00000000..f34167be
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,208 @@
+name: Build and Test
+
+on:
+ merge_group:
+ workflow_dispatch:
+ pull_request_target:
+ types:
+ - opened
+ - synchronize
+ push:
+ branches:
+ - main
+
+permissions: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+ authorize:
+ name: Authorize
+ environment: ${{ github.actor != 'dependabot[bot]' && github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && 'external' || 'internal' }}
+ runs-on: ubuntu-latest
+ steps:
+ - run: true
+
+ configure:
+ name: Configure
+ needs: [authorize]
+ runs-on: ubuntu-latest
+
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.ref }}
+
+ - id: set-matrix
+ run: echo "matrix=$(jq -c . < ./.github/workflows/matrix.json)" >> $GITHUB_OUTPUT
+
+ prepare:
+ name: Prepare Dependencies
+ needs: [configure]
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.configure.outputs.matrix) }}
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.ref }}
+
+ - uses: ./.github/actions/setup
+ with:
+ php: ${{ matrix.php }}
+
+ composer-normalize:
+ name: Composer Normalize
+ needs: [configure, prepare]
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.configure.outputs.matrix) }}
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.ref }}
+
+ - uses: ./.github/actions/setup
+ with:
+ php: ${{ matrix.php }}
+
+ - run: composer normalize --dry-run --diff
+
+ composer-validate:
+ name: Composer Validate
+ needs: [configure, prepare]
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.configure.outputs.matrix) }}
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.ref }}
+
+ - uses: ./.github/actions/setup
+ with:
+ php: ${{ matrix.php }}
+
+ - run: composer validate
+
+ pest:
+ name: PEST
+ needs: [configure, prepare]
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.configure.outputs.matrix) }}
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.ref }}
+
+ - uses: ./.github/actions/setup
+ with:
+ php: ${{ matrix.php }}
+ coverage: pcov
+
+ - if: matrix.php == '8.2'
+ run: composer pest:coverage
+
+ - if: matrix.php == '8.2'
+ uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # pin@3.1.4
+ with:
+ directory: ./coverage/
+ flags: unittestsvalidate
+
+ phpstan:
+ name: PHPStan
+ needs: [configure, prepare]
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.configure.outputs.matrix) }}
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.ref }}
+
+ - uses: ./.github/actions/setup
+ with:
+ php: ${{ matrix.php }}
+
+ - run: composer phpstan
+
+ psalm:
+ name: Psalm
+ needs: [configure, prepare]
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.configure.outputs.matrix) }}
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.ref }}
+
+ - uses: ./.github/actions/setup
+ with:
+ php: ${{ matrix.php }}
+
+ - run: composer psalm
+
+ rector:
+ name: Rector
+ needs: [configure, prepare]
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.configure.outputs.matrix) }}
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.ref }}
+
+ - uses: ./.github/actions/setup
+ with:
+ php: ${{ matrix.php }}
+
+ - run: composer rector
+
+ php-cs-fixer:
+ name: PHP CS Fixer
+ needs: [configure, prepare]
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.configure.outputs.matrix) }}
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.ref }}
+
+ - uses: ./.github/actions/setup
+ with:
+ php: ${{ matrix.php }}
+
+ - run: composer phpcs
diff --git a/.gitignore b/.gitignore
index 397959dd..852f2ef6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
build/
vendor/
+coverage/
+tmp/
.idea/
.env
.DS_Store
@@ -7,4 +9,9 @@ composer.lock
composer.phar
.phpunit.result.cache
composer.local.json
-
+composer.local.json_
+.php-cs-fixer.cache
+composer.local.old
+.vscode
+pest.log
+NOTES.md
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 00000000..6eca820a
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,231 @@
+setRiskyAllowed(true)
+ ->setRules([
+ 'array_indentation' => true,
+ 'array_push' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'assign_null_coalescing_to_coalesce_equal' => true,
+ 'backtick_to_shell_exec' => true,
+ 'binary_operator_spaces' => true,
+ 'blank_line_after_namespace' => true,
+ 'blank_line_after_opening_tag' => true,
+ 'blank_line_before_statement' => true,
+ 'blank_line_between_import_groups' => true,
+ 'braces' => true,
+ 'cast_spaces' => true,
+ 'class_attributes_separation' => ['elements' => ['const' => 'one', 'method' => 'one', 'property' => 'one', 'trait_import' => 'one', 'case' => 'one']],
+ 'class_definition' => ['multi_line_extends_each_single_line' => true, 'single_line' => true, 'single_item_single_line' => true, 'space_before_parenthesis' => false, 'inline_constructor_arguments' => false],
+ 'class_reference_name_casing' => true,
+ 'clean_namespace' => true,
+ 'combine_consecutive_issets' => true,
+ 'combine_consecutive_unsets' => true,
+ 'combine_nested_dirname' => true,
+ 'comment_to_phpdoc' => ['ignored_tags' => ['codeCoverageIgnoreStart', 'codeCoverageIgnoreEnd', 'phpstan-ignore-next-line']],
+ 'compact_nullable_typehint' => true,
+ 'concat_space' => ['spacing' => 'one'],
+ 'constant_case' => ['case' => 'lower'],
+ 'curly_braces_position' => ['control_structures_opening_brace' => 'same_line', 'functions_opening_brace' => 'next_line_unless_newline_at_signature_end', 'anonymous_functions_opening_brace' => 'same_line', 'classes_opening_brace' => 'next_line_unless_newline_at_signature_end', 'anonymous_classes_opening_brace' => 'same_line', 'allow_single_line_empty_anonymous_classes' => true, 'allow_single_line_anonymous_functions' => true],
+ 'date_time_create_from_format_call' => true,
+ 'date_time_immutable' => true,
+ 'declare_equal_normalize' => ['space' => 'none'],
+ 'declare_parentheses' => true,
+ 'declare_strict_types' => true,
+ 'dir_constant' => true,
+ 'doctrine_annotation_array_assignment' => true,
+ 'doctrine_annotation_braces' => true,
+ 'doctrine_annotation_indentation' => true,
+ 'doctrine_annotation_spaces' => true,
+ 'echo_tag_syntax' => ['format' => 'long'],
+ 'elseif' => true,
+ 'empty_loop_body' => true,
+ 'empty_loop_condition' => true,
+ 'encoding' => true,
+ 'ereg_to_preg' => true,
+ 'error_suppression' => true,
+ 'escape_implicit_backslashes' => true,
+ 'explicit_indirect_variable' => true,
+ 'explicit_string_variable' => true,
+ 'final_class' => true,
+ 'final_internal_class' => true,
+ 'final_public_method_for_abstract_class' => true,
+ 'fopen_flag_order' => true,
+ 'fopen_flags' => true,
+ 'full_opening_tag' => true,
+ 'fully_qualified_strict_types' => true,
+ 'function_declaration' => true,
+ 'function_to_constant' => true,
+ 'function_typehint_space' => true,
+ 'general_phpdoc_annotation_remove' => true,
+ 'general_phpdoc_tag_rename' => true,
+ 'get_class_to_class_keyword' => true,
+ 'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true],
+ 'group_import' => true,
+ 'heredoc_indentation' => true,
+ 'heredoc_to_nowdoc' => true,
+ 'implode_call' => true,
+ 'include' => true,
+ 'increment_style' => ['style' => 'pre'],
+ 'indentation_type' => true,
+ 'integer_literal_case' => true,
+ 'is_null' => true,
+ 'lambda_not_used_import' => true,
+ 'line_ending' => true,
+ 'linebreak_after_opening_tag' => true,
+ 'list_syntax' => ['syntax' => 'short'],
+ 'logical_operators' => true,
+ 'lowercase_cast' => true,
+ 'lowercase_keywords' => true,
+ 'lowercase_static_reference' => true,
+ 'magic_constant_casing' => true,
+ 'magic_method_casing' => true,
+ 'mb_str_functions' => false,
+ 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline', 'after_heredoc' => true],
+ 'method_chaining_indentation' => true,
+ 'modernize_strpos' => true,
+ 'modernize_types_casting' => true,
+ 'multiline_comment_opening_closing' => true,
+ 'multiline_whitespace_before_semicolons' => true,
+ 'native_function_casing' => true,
+ 'native_function_invocation' => true,
+ 'native_function_type_declaration_casing' => true,
+ 'new_with_braces' => true,
+ 'no_alias_functions' => true,
+ 'no_alias_language_construct_call' => true,
+ 'no_alternative_syntax' => true,
+ 'no_binary_string' => true,
+ 'no_blank_lines_after_class_opening' => true,
+ 'no_blank_lines_after_phpdoc' => true,
+ 'no_break_comment' => true,
+ 'no_closing_tag' => true,
+ 'no_empty_comment' => true,
+ 'no_empty_phpdoc' => true,
+ 'no_empty_statement' => true,
+ 'no_extra_blank_lines' => true,
+ 'no_homoglyph_names' => true,
+ 'no_leading_import_slash' => true,
+ 'no_leading_namespace_whitespace' => true,
+ 'no_mixed_echo_print' => true,
+ 'no_multiline_whitespace_around_double_arrow' => true,
+ 'no_multiple_statements_per_line' => true,
+ 'no_php4_constructor' => true,
+ 'no_short_bool_cast' => true,
+ 'no_singleline_whitespace_before_semicolons' => true,
+ 'no_space_around_double_colon' => true,
+ 'no_spaces_after_function_name' => true,
+ 'no_spaces_around_offset' => true,
+ 'no_spaces_inside_parenthesis' => true,
+ 'no_superfluous_elseif' => true,
+ 'no_trailing_comma_in_singleline' => true,
+ 'no_trailing_whitespace_in_comment' => true,
+ 'no_trailing_whitespace_in_string' => true,
+ 'no_trailing_whitespace' => true,
+ 'no_unneeded_control_parentheses' => true,
+ 'no_unneeded_curly_braces' => true,
+ 'no_unneeded_final_method' => true,
+ 'no_unneeded_import_alias' => true,
+ 'no_unreachable_default_argument_value' => true,
+ 'no_unset_cast' => true,
+ 'no_unused_imports' => true,
+ 'no_useless_concat_operator' => true,
+ 'no_useless_else' => true,
+ 'no_useless_nullsafe_operator' => true,
+ 'no_useless_return' => true,
+ 'no_useless_sprintf' => true,
+ 'no_whitespace_before_comma_in_array' => true,
+ 'no_whitespace_in_blank_line' => true,
+ 'non_printable_character' => true,
+ 'normalize_index_brace' => true,
+ 'not_operator_with_successor_space' => true,
+ 'nullable_type_declaration_for_default_null_value' => true,
+ 'object_operator_without_whitespace' => true,
+ 'octal_notation' => true,
+ 'operator_linebreak' => true,
+ 'ordered_class_elements' => ['sort_algorithm' => 'alpha', 'order' => ['use_trait', 'case', 'constant', 'constant_private', 'constant_protected', 'constant_public', 'property_private', 'property_private_readonly', 'property_private_static', 'property_protected', 'property_protected_readonly', 'property_protected_static', 'property_public', 'property_public_readonly', 'property_public_static', 'property_static', 'protected', 'construct', 'destruct', 'magic', 'method', 'public', 'method_public', 'method_abstract', 'method_public_abstract', 'method_public_abstract_static', 'method_public_static', 'method_static', 'method_private', 'method_private_abstract', 'method_private_abstract_static', 'method_private_static', 'method_protected', 'method_protected_abstract', 'method_protected_abstract_static', 'method_protected_static', 'phpunit', 'private', 'property']],
+ 'ordered_imports' => ['sort_algorithm' => 'alpha', 'imports_order' => ['const', 'class', 'function']],
+ 'ordered_interfaces' => true,
+ 'ordered_traits' => true,
+ 'php_unit_fqcn_annotation' => true,
+ 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false],
+ 'phpdoc_align' => ['align' => 'vertical'],
+ 'phpdoc_indent' => true,
+ 'phpdoc_inline_tag_normalizer' => true,
+ 'phpdoc_line_span' => true,
+ 'phpdoc_no_access' => true,
+ 'phpdoc_no_empty_return' => true,
+ 'phpdoc_no_package' => true,
+ 'phpdoc_no_useless_inheritdoc' => true,
+ 'phpdoc_order_by_value' => true,
+ 'phpdoc_order' => true,
+ 'phpdoc_return_self_reference' => ['replacements' => ['this' => 'self']],
+ 'phpdoc_scalar' => true,
+ 'phpdoc_separation' => true,
+ 'phpdoc_single_line_var_spacing' => true,
+ 'phpdoc_summary' => true,
+ 'phpdoc_tag_type' => true,
+ 'phpdoc_to_comment' => ['ignored_tags' => ['var']],
+ 'phpdoc_trim_consecutive_blank_line_separation' => true,
+ 'phpdoc_trim' => true,
+ 'phpdoc_types_order' => true,
+ 'phpdoc_types' => true,
+ 'phpdoc_var_annotation_correct_order' => true,
+ 'phpdoc_var_without_name' => true,
+ 'pow_to_exponentiation' => true,
+ 'protected_to_private' => true,
+ 'psr_autoloading' => true,
+ 'random_api_migration' => true,
+ 'regular_callable_call' => true,
+ 'return_assignment' => true,
+ 'return_type_declaration' => ['space_before' => 'none'],
+ 'return_type_declaration' => true,
+ 'self_accessor' => true,
+ 'self_static_accessor' => true,
+ 'semicolon_after_instruction' => true,
+ 'set_type_to_cast' => true,
+ 'short_scalar_cast' => true,
+ 'simple_to_complex_string_variable' => true,
+ 'simplified_if_return' => true,
+ 'single_blank_line_at_eof' => true,
+ 'single_blank_line_before_namespace' => true,
+ 'single_class_element_per_statement' => true,
+ 'single_line_after_imports' => true,
+ 'single_line_comment_spacing' => true,
+ 'single_line_comment_style' => ['comment_types' => ['hash']],
+ 'single_line_throw' => true,
+ 'single_quote' => true,
+ 'single_space_after_construct' => true,
+ 'single_space_around_construct' => true,
+ 'single_trait_insert_per_statement' => true,
+ 'space_after_semicolon' => true,
+ 'standardize_increment' => true,
+ 'standardize_not_equals' => true,
+ 'statement_indentation' => true,
+ 'static_lambda' => true,
+ 'strict_comparison' => true,
+ 'strict_param' => true,
+ 'string_length_to_empty' => true,
+ 'string_line_ending' => true,
+ 'switch_case_semicolon_to_colon' => true,
+ 'switch_case_space' => true,
+ 'switch_continue_to_break' => true,
+ 'ternary_operator_spaces' => true,
+ 'ternary_to_elvis_operator' => true,
+ 'ternary_to_null_coalescing' => true,
+ 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arguments', 'arrays', 'match', 'parameters']],
+ 'trim_array_spaces' => true,
+ 'types_spaces' => ['space' => 'single', 'space_multiple_catch' => 'single'],
+ 'unary_operator_spaces' => true,
+ 'use_arrow_functions' => true,
+ 'visibility_required' => true,
+ 'void_return' => true,
+ 'whitespace_after_comma_in_array' => true,
+ 'yoda_style' => true,
+ ])
+ ->setFinder(
+ PhpCsFixer\Finder::create()
+ ->exclude('vendor')
+ ->in([__DIR__ . '/src/', __DIR__ . '/deprecated/']),
+ );
diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
new file mode 100644
index 00000000..993f321e
--- /dev/null
+++ b/.phpcs.xml.dist
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.semgrepignore b/.semgrepignore
new file mode 100644
index 00000000..37dc6e8d
--- /dev/null
+++ b/.semgrepignore
@@ -0,0 +1,5 @@
+.github/
+docs/
+examples/
+tests/
+\*.md
diff --git a/.shiprc b/.shiprc
index 579ae8f7..efff7fc8 100644
--- a/.shiprc
+++ b/.shiprc
@@ -1,6 +1,7 @@
{
"files": {
- "src/Auth0.php": []
+ "src/ServiceAbstract.php": [],
+ ".version": []
},
"prefixVersion": false
}
diff --git a/.styleci.yml b/.styleci.yml
deleted file mode 100644
index 6f1da912..00000000
--- a/.styleci.yml
+++ /dev/null
@@ -1,258 +0,0 @@
-risky: true
-version: 8.0
-preset: none
-enabled:
- - align_double_arrow
- - align_phpdoc
- - alpha_ordered_imports
- - alpha_ordered_traits
- - array_indentation
- - array_push
- - assign_null_coalescing_to_coalesce_equal
- - backtick_to_shell_exec
- - binary_operator_spaces
- - blank_line_after_namespace
- - blank_line_after_opening_tag
- - blank_line_before_break
- - blank_line_before_cases
- - blank_line_before_continue
- - blank_line_before_declare
- - blank_line_before_do
- - blank_line_before_exit
- - blank_line_before_for
- - blank_line_before_goto
- - blank_line_before_if
- - blank_line_before_include
- - blank_line_before_return
- - blank_line_before_switch
- - blank_line_before_throw
- - blank_line_before_try
- - blank_line_before_while
- - blank_line_before_yield
- - cast_spaces
- - class_reference_name_casing
- - clean_namespace
- - combine_consecutive_issets
- - combine_consecutive_unsets
- - combine_nested_dirname
- - comment_to_phpdoc
- - compact_nullable_typehint
- - concat_with_spaces
- - const_separation
- - const_visibility_required
- - declare_equal_normalize
- - declare_strict_types
- - deprecation_error_suppression
- - die_to_exit
- - dir_constant
- - doctrine_annotation_array_assignment
- - doctrine_annotation_braces
- - doctrine_annotation_indentation
- - doctrine_annotation_spaces
- - elseif
- - empty_loop_body_braces
- - empty_loop_condition
- - encoding
- - ereg_to_preg
- - escape_implicit_backslashes
- - explicit_indirect_variable
- - explicit_string_variable
- - final_internal_class
- - final_public_method_for_abstract_class
- - fopen_flag_order
- - fopen_flags
- - full_opening_tag
- - fully_qualified_strict_types
- - function_declaration
- - function_to_constant
- - function_typehint_space
- - get_class_to_class_keyword
- - hash_to_slash_comment
- - heredoc_indentation
- - heredoc_to_nowdoc
- - implode_call
- - include
- - indentation
- - integer_literal_case
- - is_null
- - laravel_braces
- - laravel_phpdoc_alignment
- - laravel_phpdoc_order
- - laravel_phpdoc_separation
- - linebreak_after_opening_tag
- - logical_operators
- - lowercase_cast
- - lowercase_constants
- - lowercase_keywords
- - lowercase_static_reference
- - magic_constant_casing
- - magic_method_casing
- - mb_str_functions
- - method_argument_space_strict
- - method_chaining_indentation
- - method_separation
- - method_visibility_required
- - modernize_strpos
- - modernize_types_casting
- - multiline_comment_opening_closing
- - native_function_casing
- - native_function_invocation_symfony
- - native_function_type_declaration_casing
- - new_with_braces
- - no_alias_functions
- - no_alternative_syntax
- - no_binary_string
- - no_blank_lines_after_class_opening
- - no_blank_lines_after_phpdoc
- - no_blank_lines_after_return
- - no_blank_lines_after_throw
- - no_blank_lines_around_break
- - no_blank_lines_around_cases
- - no_blank_lines_around_continue
- - no_blank_lines_around_switch
- - no_blank_lines_between_imports
- - no_break_comment
- - no_closing_tag
- - no_empty_comment
- - no_empty_phpdoc
- - no_empty_statement
- - no_extra_block_blank_lines
- - no_extra_consecutive_blank_lines
- - no_homoglyph_names
- - no_leading_import_slash
- - no_leading_namespace_whitespace
- - no_multiline_whitespace_around_double_arrow
- - no_multiline_whitespace_before_semicolons
- - no_php4_constructor
- - no_short_bool_cast
- - no_short_echo_tag
- - no_singleline_whitespace_before_semicolons
- - no_space_around_double_colon
- - no_spaces_after_function_name
- - no_spaces_inside_offset
- - no_spaces_inside_parenthesis
- - no_spaces_outside_offset
- - no_superfluous_elseif
- - no_superfluous_phpdoc_tags_symfony
- - no_trailing_comma_in_list_call
- - no_trailing_comma_in_singleline_array
- - no_trailing_comma_in_singleline_function_call
- - no_trailing_whitespace
- - no_trailing_whitespace_in_comment
- - no_trailing_whitespace_in_string
- - no_unneeded_control_parentheses
- - no_unneeded_curly_braces
- - no_unneeded_final_method
- - no_unneeded_import_alias
- - no_unreachable_default_argument_value
- - no_unset_cast
- - no_unused_imports
- - no_unused_lambda_imports
- - no_useless_else
- - no_useless_return
- - no_useless_sprintf
- - no_whitespace_before_comma_in_array
- - no_whitespace_in_blank_line
- - non_printable_character
- - normalize_index_brace
- - not_operator_with_successor_space
- - nullable_type_declarations
- - object_operator_without_whitespace
- - operator_linebreak_end
- - ordered_class_elements
- - php_unit_fqcn_annotation
- - phpdoc_add_missing_param_annotation
- - phpdoc_annotation_without_dot
- - phpdoc_indent
- - phpdoc_inline_inheritdoc
- - phpdoc_inline_tag_normalizer
- - phpdoc_link_to_see
- - phpdoc_no_access
- - phpdoc_no_empty_return
- - phpdoc_no_package
- - phpdoc_no_useless_inheritdoc
- - phpdoc_property
- - phpdoc_return_self_reference
- - phpdoc_scalar
- - phpdoc_single_line_var_spacing
- - phpdoc_singular_inheritdoc
- - phpdoc_summary
- - phpdoc_to_comment
- - phpdoc_trim
- - phpdoc_trim_consecutive_blank_line_separation
- - phpdoc_type_to_var
- - phpdoc_types
- - phpdoc_types_order
- - phpdoc_var_order
- - phpdoc_var_without_name
- - pow_to_exponentiation
- - pre_increment
- - print_to_echo
- - property_separation
- - property_visibility_required
- - protected_to_private
- - psr4
- - random_api_migration
- - regular_callable_call
- - return_assignment
- - return_type_declaration
- - self_accessor
- - self_static_accessor
- - semicolon_after_instruction
- - set_type_to_cast
- - short_array_syntax
- - short_list_syntax
- - short_scalar_cast
- - simple_to_complex_string_variable
- - simplified_if_return
- - single_blank_line_at_eof
- - single_blank_line_before_namespace
- - single_class_element_per_statement
- - single_import_per_statement
- - single_line_after_imports
- - single_line_comment_spacing
- - single_line_throw
- - single_quote
- - single_space_after_construct
- - single_trait_insert_per_statement
- - space_after_semicolon
- - standardize_increment
- - standardize_not_equals
- - static_lambda
- - strict_comparison
- - strict_param
- - string_length_to_empty
- - string_line_ending
- - switch_case_semicolon_to_colon
- - switch_case_space
- - switch_continue_to_break
- - symfony_class_definition
- - ternary_operator_spaces
- - ternary_to_elvis_operator
- - ternary_to_null_coalescing
- - trailing_comma_in_multiline_array
- - trailing_comma_in_multiline_call
- - trailing_comma_in_multiline_definition
- - trim_array_spaces
- - unalign_equals
- - unary_operator_spaces
- - union_type_without_spaces
- - unix_line_endings
- - use_arrow_functions
- - void_return
- - whitespace_after_comma_in_array
- - yoda_style
-finder:
- exclude:
- - "modules"
- - "node_modules"
- - "nova"
- - "nova-components"
- - "storage"
- - "spark"
- - "vendor"
- - "tests"
- name: "*.php"
- not-name:
- - "*.blade.php"
- - "_ide_helper.php"
diff --git a/.version b/.version
new file mode 100644
index 00000000..e465da43
--- /dev/null
+++ b/.version
@@ -0,0 +1 @@
+7.14.0
diff --git a/CHANGELOG.ARCHIVE.md b/CHANGELOG.ARCHIVE.md
new file mode 100644
index 00000000..23d2762d
--- /dev/null
+++ b/CHANGELOG.ARCHIVE.md
@@ -0,0 +1,367 @@
+# Changelog Archive
+
+This file contains changes for all versions of this package prior to the latest major, 7.0.
+
+The changelog for the latest changes is [CHANGELOG.md](./CHANGELOG.md).
+
+## [6.5.0](https://github.com/auth0/laravel-auth0/tree/6.5.0) (2021-10-15)
+
+### Added
+
+- Add SDK alias methods for passwordless endpoints [\#228](https://github.com/auth0/laravel-auth0/pull/228)
+
+## [6.4.1](https://github.com/auth0/laravel-auth0/tree/6.4.0) (2021-08-02)
+
+### Fixed
+
+- Use the fully qualified facade class names [\#215](https://github.com/auth0/laravel-auth0/pull/215)
+- Update auth0-PHP dependency [\#222](https://github.com/auth0/laravel-auth0/pull/222)
+- Pass api_identifier config as audience to Auth0\SDK\Auth0 [\#214](https://github.com/auth0/laravel-auth0/pull/214)
+
+## [6.4.0](https://github.com/auth0/laravel-auth0/tree/6.4.0) (2021-03-25)
+
+### Improved
+
+- Add support for Auth0 Organizations [\#209](https://github.com/auth0/laravel-auth0/pull/209)
+
+## [6.3.0](https://github.com/auth0/laravel-auth0/tree/6.3.0) (2020-02-18)
+
+### Improved
+
+- Store changes made to the user object during the onLogin event hook [\#206](https://github.com/auth0/laravel-auth0/pull/206)
+
+### Fixed
+
+- Avoid throwing an error when calling getUserByUserInfo() during login callback event when the supplied profile is empty/null [\#207](https://github.com/auth0/laravel-auth0/pull/207)
+
+## [6.2.0](https://github.com/auth0/laravel-auth0/tree/6.2.0) (2020-01-15)
+
+### Added
+
+- Support PHP 8.0 [\#200](https://github.com/auth0/laravel-auth0/pull/200)
+
+### Fixed
+
+- Fix the missing `return null;` in `getUserByIdentifier` [\#201](https://github.com/auth0/laravel-auth0/pull/201)
+
+## [6.1.0](https://github.com/auth0/laravel-auth0/tree/6.1.0) (2020-09-17)
+
+### Added
+
+- Support Laravel 8 [\#190](https://github.com/auth0/laravel-auth0/pull/190)
+
+### Fixed
+
+- Fix composer.json whitespace issue [\#192](https://github.com/auth0/laravel-auth0/pull/192)
+
+## [6.0.1](https://github.com/auth0/laravel-auth0/tree/6.0.1) (2020-04-28)
+
+### Fixed
+
+- Fix access token decoding and validation [\#183](https://github.com/auth0/laravel-auth0/pull/183)
+
+## [6.0.0](https://github.com/auth0/laravel-auth0/tree/6.0.0) (2020-04-09)
+
+**This is a major release and includes breaking changes!** This release also includes a major version change for the PHP SDK that it relies on. Please see the [migration guide](https://github.com/auth0/auth0-PHP/blob/master/MIGRATE-v5-TO-v7.md) for the PHP SDK for more information.
+
+### Added
+
+- auth0-PHP 7.0 - State and nonce handling [\#163](https://github.com/auth0/laravel-auth0/issues/163)
+- Implement auth0 guard [\#166](https://github.com/auth0/laravel-auth0/pull/166)
+
+### Improved
+
+- Use array for Auth0JWTUser and add repo return types [\#176](https://github.com/auth0/laravel-auth0/pull/176)
+- Update PHP SDK to v7.0.0 [\#162](https://github.com/auth0/laravel-auth0/pull/162)
+- Bind SessionState handler interface in container [\#147](https://github.com/auth0/laravel-auth0/pull/147)
+
+### Fixed
+
+- Fix Laravel session management [\#174](https://github.com/auth0/laravel-auth0/pull/174)
+- Cannot use actingAs unit tests functionality [\#161](https://github.com/auth0/laravel-auth0/issues/161)
+
+## [5.4.0](https://github.com/auth0/laravel-auth0/tree/5.4.0) (2020-03-27)
+
+### Added
+
+- Laravel 7 support [\#167](https://github.com/auth0/laravel-auth0/pull/167)
+
+### Fixed
+
+- Laravel 7.0 supported release. [\#171](https://github.com/auth0/laravel-auth0/issues/171)
+- Fixed PHPDocs [\#170](https://github.com/auth0/laravel-auth0/pull/170)
+
+## [5.3.1](https://github.com/auth0/laravel-auth0/tree/5.3.1) (2019-11-14)
+
+### Fixed
+
+- Setting of state_handler in Auth0Service causes "Invalid state" error [\#154](https://github.com/auth0/laravel-auth0/issues/154)
+- Allow store and state_handler to be passed in from config [\#156](https://github.com/auth0/laravel-auth0/pull/156)
+- Add 'persist_refresh_token' key to laravel-auth0 configuration file. [\#152](https://github.com/auth0/laravel-auth0/pull/152)
+- Replace `setEnvironment` with `setEnvProperty` [\#145](https://github.com/auth0/laravel-auth0/pull/145)
+
+## [5.3.0](https://github.com/auth0/laravel-auth0/tree/5.3.0) (2019-09-26)
+
+### Added
+
+- Support Laravel 6 [\#139](https://github.com/auth0/laravel-auth0/pull/139)
+- Feature request: Add Laravel 6 support [\#138](https://github.com/auth0/laravel-auth0/issues/138)
+
+### Fixed
+
+- Use LaravelSessionStore in the SessionStateHandler. [\#135](https://github.com/auth0/laravel-auth0/pull/135)
+- SessionStateHandler should use LaravelSessionStore not SessionStore [\#125](https://github.com/auth0/laravel-auth0/issues/125)
+
+## [5.2.0](https://github.com/auth0/laravel-auth0/tree/5.2.0) (2019-06-27)
+
+### Added
+
+- Authenticate as a Laravel API user using the Auth0 token [\#129](https://github.com/auth0/laravel-auth0/issues/129)
+- Redirect to previous page after login [\#122](https://github.com/auth0/laravel-auth0/issues/122)
+- Auth0User uses private variables so they cannot be accessed or overridden in child class [\#120](https://github.com/auth0/laravel-auth0/issues/120)
+- API routes broken in auth0-laravel-php-web-app (and in general)? [\#117](https://github.com/auth0/laravel-auth0/issues/117)
+- API returning "token algorithm not supported" [\#116](https://github.com/auth0/laravel-auth0/issues/116)
+- Changing name of user identifier [\#115](https://github.com/auth0/laravel-auth0/issues/115)
+- Possible to use User object functions? [\#114](https://github.com/auth0/laravel-auth0/issues/114)
+- Auth0-PHP@5.3.1 breaks Laravel-Auth0 [\#108](https://github.com/auth0/laravel-auth0/issues/108)
+- Extend Illuminate\Foundation\Auth\User [\#104](https://github.com/auth0/laravel-auth0/issues/104)
+- [Bug] Inconsistencies with the singleton Auth0Service [\#103](https://github.com/auth0/laravel-auth0/issues/103)
+- How do you combine Auth0 Lock with Laravel Auth0? [\#102](https://github.com/auth0/laravel-auth0/issues/102)
+- OnLogin callback question [\#97](https://github.com/auth0/laravel-auth0/issues/97)
+- Add composer.lock file [\#123](https://github.com/auth0/laravel-auth0/pull/123) ([lbalmaceda](https://github.com/lbalmaceda))
+
+### Improved
+
+- Change private properties to protected [\#132](https://github.com/auth0/laravel-auth0/pull/132)
+- Return null instead of false in Auth0UserProvider. [\#128](https://github.com/auth0/laravel-auth0/pull/128)
+- Change the visibility of the getter method from private to public [\#121](https://github.com/auth0/laravel-auth0/pull/121)
+- Updated required PHP version to 5.4 in composer [\#118](https://github.com/auth0/laravel-auth0/pull/118)
+- Changed arrays to use short array syntax [\#110](https://github.com/auth0/laravel-auth0/pull/110)
+
+### Fixed
+
+- Fix cachehandler resolving issues [\#131](https://github.com/auth0/laravel-auth0/pull/131)
+- Added the Auth0Service as a singleton through the classname [\#107](https://github.com/auth0/laravel-auth0/pull/107)
+- Fixed typo [\#106](https://github.com/auth0/laravel-auth0/pull/106)
+
+## [5.1.0](https://github.com/auth0/laravel-auth0/tree/5.1.0) (2018-03-20)
+
+### Added
+
+- AutoDiscovery [\#91](https://github.com/auth0/laravel-auth0/pull/91) ([m1guelpf](https://github.com/m1guelpf))
+- Added guzzle options to config to allow for connection options [\#88](https://github.com/auth0/laravel-auth0/pull/88)
+
+### Improved
+
+- Change default settings file [\#96](https://github.com/auth0/laravel-auth0/pull/96)
+- Utilise Auth0->Login to ensure state validation [\#90](https://github.com/auth0/laravel-auth0/pull/90)
+
+### Fixed
+
+- Make code comments gender neutral [\#98](https://github.com/auth0/laravel-auth0/pull/98)
+- Fix README and CHANGELOG [\#99](https://github.com/auth0/laravel-auth0/pull/99)
+- pls change config arg name [\#95](https://github.com/auth0/laravel-auth0/issues/95)
+
+## [5.0.2](https://github.com/auth0/laravel-auth0/tree/5.0.2) (2017-08-30)
+
+### Fixed
+
+- Use instead of to identify the Auth0 user [\#80](https://github.com/auth0/laravel-auth0/pull/80)
+
+## [5.0.1](https://github.com/auth0/laravel-auth0/tree/5.0.1) (2017-02-23)
+
+### Fixed
+
+- Fixed `supported_algs` configuration name
+
+## [5.0.0](https://github.com/auth0/laravel-auth0/tree/5.0.0) (2017-02-22)
+
+### Fixed
+
+- V5: update to auth0 sdk v5 [\#69](https://github.com/auth0/laravel-auth0/pull/69)
+
+## [4.0.8](https://github.com/auth0/laravel-auth0/tree/4.0.8) (2017-01-27)
+
+### Fixed
+
+- Allow use of RS256 Protocol [\#63](https://github.com/auth0/wp-auth0/issues/63)
+- Add RS256 to the list of supported algorithms [\#62](https://github.com/auth0/wp-auth0/issues/62)
+- allow to configure the algorithm supported for token verification [\#65](https://github.com/auth0/laravel-auth0/pull/65)
+
+## [4.0.7](https://github.com/auth0/laravel-auth0/tree/4.0.7) (2017-01-02)
+
+### Fixed
+
+- it should pass all the configs to the oauth client [\#64](https://github.com/auth0/laravel-auth0/pull/64)
+
+## [4.0.6](https://github.com/auth0/laravel-auth0/tree/4.0.6) (2016-11-29)
+
+### Fixed
+
+- Code style & docblocks [\#56](https://github.com/auth0/laravel-auth0/pull/56)
+- Adding accessor to retrieve JWT from Auth0Service [\#58](https://github.com/auth0/laravel-auth0/pull/58)
+
+## [4.0.5](https://github.com/auth0/laravel-auth0/tree/4.0.5) (2016-11-29)
+
+### Fixed
+
+- Added flag for not encoded tokens + removed example [\#57](https://github.com/auth0/laravel-auth0/pull/57)
+
+## [4.0.4](https://github.com/auth0/laravel-auth0/tree/4.0.4) (2016-11-25)
+
+### Fixed
+
+- Fixing config type [\#55](https://github.com/auth0/laravel-auth0/pull/55)
+
+## [4.0.2](https://github.com/auth0/laravel-auth0/tree/4.0.2) (2016-10-03)
+
+### Fixed
+
+- Fixing JWTVerifier [\#54](https://github.com/auth0/laravel-auth0/pull/54)
+
+## [4.0.1](https://github.com/auth0/laravel-auth0/tree/4.0.1) (2016-09-19)
+
+### Fixed
+
+- Fix error becuase of contract and class with the same name [\#52](https://github.com/auth0/laravel-auth0/pull/52)
+
+## [4.0.0](https://github.com/auth0/laravel-auth0/tree/4.0.0) (2016-09-15)
+
+### Improved
+
+- Better support for Laravel 5.3: Support for Laravel Passport for token verification
+ Support of auth0 PHP sdk v4 with JWKs cache
+
+### Fixed
+
+- Merge pull request #50 from auth0/4.x.x-dev [\#50](https://github.com/auth0/laravel-auth0/pull/50)
+
+## [3.2.1](https://github.com/auth0/laravel-auth0/tree/3.2.1) (2016-09-12)
+
+### Fixed
+
+- Fix for Laravel 5.2 [\#49](https://github.com/auth0/laravel-auth0/pull/49)
+
+## [3.2.0](https://github.com/auth0/laravel-auth0/tree/3.2.0) (2016-07-11)
+
+### Fixed
+
+- New optional jwt middleware [\#40](https://github.com/auth0/laravel-auth0/pull/40)
+
+## [3.1.0](https://github.com/auth0/laravel-auth0/tree/3.1.0) (2016-05-02)
+
+### Fixed
+
+- 3.1.0 [\#36](https://github.com/auth0/laravel-auth0/pull/36)
+
+## [3.0.3](https://github.com/auth0/laravel-auth0/tree/3.0.3) (2016-01-28)
+
+### Fixed
+
+- Tag 2.2.2 breaks on Laravel 5.1 [\#30](https://github.com/auth0/laravel-auth0/issues/30)
+- Conform to 5.2's Authenticatable contract [\#31](https://github.com/auth0/laravel-auth0/pull/31)
+
+## [3.0.2](https://github.com/auth0/laravel-auth0/tree/3.0.2) (2016-01-25)
+
+### Fixed
+
+- Added optional persistence configuration values [\#29](https://github.com/auth0/laravel-auth0/pull/29)
+
+## [2.2.1](https://github.com/auth0/laravel-auth0/tree/2.2.1) (2016-01-22)
+
+### Fixed
+
+- Create a logout route [\#25](https://github.com/auth0/laravel-auth0/issues/25)
+- Auth0 SDK checks for null values instead of false [\#27](https://github.com/auth0/laravel-auth0/pull/27)
+
+## [3.0.1](https://github.com/auth0/laravel-auth0/tree/3.0.1) (2016-01-18)
+
+### Fixed
+
+- updated auth0-php dependency [\#24](https://github.com/auth0/laravel-auth0/pull/24)
+
+## [3.0.0](https://github.com/auth0/laravel-auth0/tree/3.0.0) (2016-01-06)
+
+### Fixed
+
+- auth0/auth0-php ~1.0 requirement doesn't support latest GuzzleHttp [\#21](https://github.com/auth0/laravel-auth0/issues/21)
+- updated to be compatible with laravel 5.2 [\#23](https://github.com/auth0/laravel-auth0/pull/23)
+
+## [2.2.0](https://github.com/auth0/laravel-auth0/tree/2.2.0) (2015-11-30)
+
+### Fixed
+
+- updated auth0-php dependency version [\#22](https://github.com/auth0/laravel-auth0/pull/22)
+- Update login.blade.php [\#20](https://github.com/auth0/laravel-auth0/pull/20)
+
+## [2.1.4](https://github.com/auth0/laravel-auth0/tree/2.1.4) (2015-10-27)
+
+### Fixed
+
+- Middleware contract has been deprecated in 5.1 [\#19](https://github.com/auth0/laravel-auth0/pull/19)
+- Fixed some typo's in the comments. [\#18](https://github.com/auth0/laravel-auth0/pull/18)
+- Removed note about unstable dependency from README [\#17](https://github.com/auth0/laravel-auth0/pull/17)
+- Update composer instructions [\#16](https://github.com/auth0/laravel-auth0/pull/16)
+- Use a tagged release of adoy/oauth2 [\#15](https://github.com/auth0/laravel-auth0/pull/15)
+
+## [2.1.3](https://github.com/auth0/laravel-auth0/tree/2.1.3) (2015-07-17)
+
+### Fixed
+
+- updated jwt dependency [\#14](https://github.com/auth0/laravel-auth0/pull/14)
+
+## [2.1.2](https://github.com/auth0/laravel-auth0/tree/2.1.2) (2015-05-15)
+
+### Fixed
+
+- Added override of info headers [\#13](https://github.com/auth0/laravel-auth0/pull/13)
+
+## [2.1.1](https://github.com/auth0/laravel-auth0/tree/2.1.1) (2015-05-12)
+
+### Fixed
+
+- SDK Client headers spec compliant [\#11](https://github.com/auth0/laravel-auth0/issues/11)
+- Support for Laravel 5? [\#6](https://github.com/auth0/laravel-auth0/issues/6)
+- SDK Client headers spec compliant \#11 [\#12](https://github.com/auth0/laravel-auth0/pull/12)
+
+## [2.1.0](https://github.com/auth0/laravel-auth0/tree/2.1.0) (2015-05-07)
+
+### Fixed
+
+- Upgrade to auth-php 1.0.0: Added support to API V2 [\#10](https://github.com/auth0/laravel-auth0/pull/10)
+
+## [2.0.0](https://github.com/auth0/laravel-auth0/tree/2.0.0) (2015-04-20)
+
+### Fixed
+
+- Package V2 for Laravel5 [\#9](https://github.com/auth0/laravel-auth0/pull/9)
+
+## [1.0.8](https://github.com/auth0/laravel-auth0/tree/1.0.8) (2015-04-14)
+
+- [Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.7...1.0.8)
+
+## [1.0.7](https://github.com/auth0/laravel-auth0/tree/1.0.7) (2015-04-13)
+
+### Fixed
+
+- Fixed the way the access token is pased to the A0User [\#7](https://github.com/auth0/laravel-auth0/pull/7)
+- Update README.md [\#5](https://github.com/auth0/laravel-auth0/pull/5)
+
+## [1.0.6](https://github.com/auth0/laravel-auth0/tree/1.0.6) (2014-08-01)
+
+- [Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.5...1.0.6)
+
+## [1.0.5](https://github.com/auth0/laravel-auth0/tree/1.0.5) (2014-08-01)
+
+### Fixed
+
+- Problem with normal laravel user table [\#4](https://github.com/auth0/laravel-auth0/issues/4)
+- Update README.md [\#3](https://github.com/auth0/laravel-auth0/pull/3)
+
+## [1.0.4](https://github.com/auth0/laravel-auth0/tree/1.0.4) (2014-05-07)
+
+- [Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.3...1.0.4)
+
+## [1.0.3](https://github.com/auth0/laravel-auth0/tree/1.0.3) (2014-04-21)
+
+- [Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.0...1.0.3)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a249f218..ca696394 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,588 +1,305 @@
# Change Log
-## [7.3.0](https://github.com/auth0/laravel-auth0/tree/7.3.0) (2022-11-07)
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/7.2.2...7.3.0)
-
-**Added**
-- add: Raise additional Laravel Auth Events [\#331](https://github.com/auth0/laravel-auth0/pull/331) ([evansims](https://github.com/evansims))
-
-**Fixed**
-- fix: `env()` incorrectly assigns `cookieExpires` to a `string` value [\#332](https://github.com/auth0/laravel-auth0/pull/332) ([evansims](https://github.com/evansims))
-- fix: Auth0\Laravel\Cache\LaravelCachePool::createItem returning a cache miss [\#329](https://github.com/auth0/laravel-auth0/pull/329) ([pkivits-litebit](https://github.com/pkivits-litebit))
-
-## [7.2.2](https://github.com/auth0/laravel-auth0/tree/7.2.2) (2022-10-19)
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/7.2.1...7.2.2)
+## [7.14.0](https://github.com/auth0/laravel-auth0/tree/7.14.0) (2024-04-01)
+[Full Changelog](https://github.com/auth0/laravel-auth0/compare/7.13.0...7.14.0)
-**Fixed**
-- [SDK-3720] Restore `php artisan vendor:publish` command [\#321](https://github.com/auth0/laravel-auth0/pull/321) ([evansims](https://github.com/evansims))
-- [SDK-3721] Bump minimum `auth0/auth0-php` version to `^8.3.4` [\#322](https://github.com/auth0/laravel-auth0/pull/322) ([evansims](https://github.com/evansims))
+**Changed**
+- refactor: add additional Telescope state check [\#447](https://github.com/auth0/laravel-auth0/pull/447) ([samuelhgf](https://github.com/samuelhgf))
+- chore(deps): replace temporary `psalm-laravel-plugin` fork with official [\#448](https://github.com/auth0/laravel-auth0/pull/448) ([alies-dev](https://github.com/alies-dev))
-## [7.2.1](https://github.com/auth0/laravel-auth0/tree/7.2.1) (2022-10-13)
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/7.2.0...7.2.1)
+## [7.13.0](https://github.com/auth0/laravel-auth0/tree/7.12.0) (2024-03-11)
-**Fixed**
-- `Auth0\Laravel\Auth0` no longer requires a session configuration for stateless strategies, restoring previous behavior. [\#317](https://github.com/auth0/laravel-auth0/pull/317) ([evansims](https://github.com/evansims))
-- The SDK now requires `^3.0` of the `psr/cache` dependency, to accomodate breaking changes made in the upstream interface (typed parameters and return types) for PHP 8.0+. [\#316](https://github.com/auth0/laravel-auth0/pull/316) ([evansims](https://github.com/evansims))
+[Full Changelog](https://github.com/auth0/laravel-auth0/compare/7.12.0...7.13.0)
-## [7.2.0](https://github.com/auth0/laravel-auth0/tree/7.2.0) (2022-10-10)
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/7.1.0...7.2.0)
+**Added**
-Thank you to [tonyfox-disguise](https://github.com/tonyfox-disguise), [jeovajr](https://github.com/jeovajr) and [nie7321](https://github.com/nie7321) for their contributions to this release.
+- Add support for Laravel 11 [\#445](https://github.com/auth0/laravel-auth0/pull/445) ([evansims](https://github.com/evansims))
**Changed**
-- `Auth0\Laravel\Store\LaravelSession` has been added as the default `sessionStorage` and `transientStorage` interfaces for the underlying [Auth0-PHP SDK](https://github.com/auth0/auth0-PHP/). The SDK now leverages the native [Laravel Session APIs](https://laravel.com/docs/9.x/session) by default. [\#307](https://github.com/auth0/laravel-auth0/pull/307) ([evansims](https://github.com/evansims))¹
-- `Auth0\Laravel\Cache\LaravelCachePool` and `Auth0\Laravel\Cache\LaravelCacheItem` have been added as the default `tokenCache` and `managementTokenCache` interfaces for the underlying [Auth0-PHP SDK](https://github.com/auth0/auth0-PHP/). The SDK now leverages the native [Laravel Cache APIs](https://laravel.com/docs/9.x/cache) by default. [\#307](https://github.com/auth0/laravel-auth0/pull/307) ([evansims](https://github.com/evansims))
-- `Auth0\Laravel\Auth\Guard` now supports the `viaRemember` method. [\#306](https://github.com/auth0/laravel-auth0/pull/306) ([tonyfox-disguise](https://github.com/tonyfox-disguise))
-- `Auth0\Laravel\Http\Middleware\Stateless\Authorize` now returns a 401 status instead of 403 for unauthenticated users. [\#304](https://github.com/auth0/laravel-auth0/issues/304) ([jeovajr](https://github.com/jeovajr))
-- PHP 8.0 is now the minimum supported runtime version. Please review the [README](README.md) for more information on support windows.
-
-¹ This change may require your application's users to re-authenticate. You can avoid this by changing the `sessionStorage` and `transientStorage` options in your SDK configuration to their previous default instances of `Auth0\SDK\Store\CookieStore`, but it is recommended you migrate to the new `LaravelSession` default.
-## [7.1.0](https://github.com/auth0/laravel-auth0/tree/7.1.0) (2022-08-08)
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/7.0.1...7.1.0)
+- Verify that Telescope is enabled via configuration helper [\#444](https://github.com/auth0/laravel-auth0/pull/444) ([samuelhgf](https://github.com/samuelhgf))
-**Changed**
-- [SDK-3576] Return interfaces instead of concrete classes [\#296](https://github.com/auth0/laravel-auth0/pull/296) ([evansims](https://github.com/evansims))
-- change: Use class names for app() calls [\#291](https://github.com/auth0/laravel-auth0/pull/291) ([evansims](https://github.com/evansims))
+## [7.12.0](https://github.com/auth0/laravel-auth0/tree/7.12.0) (2023-12-07)
-**Fixed**
-- [SDK-3585] Fix: `Missing Code` error on Callback Route for Octane Customers [\#297](https://github.com/auth0/laravel-auth0/pull/297) ([evansims](https://github.com/evansims))
+[Full Changelog](https://github.com/auth0/laravel-auth0/compare/7.11.0...7.12.0)
-## [7.0.1](https://github.com/auth0/laravel-auth0/tree/7.0.1) (2022-06-01)
+**Added**
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/7.0.0...7.0.1)
+- Implement support for Back-Channel Logout [\#435](https://github.com/auth0/laravel-auth0/pull/435) ([evansims](https://github.com/evansims))
+- Restore configurable route paths [\#436](https://github.com/auth0/laravel-auth0/pull/436) ([evansims](https://github.com/evansims))
**Fixed**
-- Fixed an issue in `Auth0\Laravel\Http\Controller\Stateful\Callback` where `$errorDescription`'s value was assigned an incorrect value when an error was encountered. [\#266](https://github.com/auth0/laravel-auth0/pull/288) ([evansims](https://github.com/evansims))
-
-**Closed Issues**
-- Resolves [\#287](https://github.com/auth0/laravel-auth0/issues/287) ([piljac1](https://github.com/piljac1))
+- Resolve `CacheBridgeAbstract::save()` not storing values when cache misses [\#434](https://github.com/auth0/laravel-auth0/pull/434) ([seruymt](https://github.com/seruymt))
-## [7.0.0](https://github.com/auth0/laravel-auth0/tree/7.0.0) (2022-03-21)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.5.0...7.0.0)
-
-Auth0 Laravel SDK v7 includes many significant changes over previous versions:
-
-- Support for Laravel 9.
-- Support for Auth0-PHP SDK 8.
-- New authentication route controllers for plug-and-play login support.
-- Improved authentication middleware for regular web applications.
-- New authorization middleware for token-based backend API applications.
-
-As expected with a major release, Auth0 Laravel SDK v7 includes breaking changes. Please review the [upgrade guide](UPGRADE.md) thoroughly to understand the changes required to migrate your application to v7.
-
-**Breaking Changes Summary**
-
-- Namespace has been updated from `Auth0\Login` to `Auth0\Laravel`
-- Auth0-PHP SDK dependency updated to V8
-- New configuration format
-- SDK now self-registers its services and middleware
-- New UserProvider API
-
-## [7.0.0-BETA2](https://github.com/auth0/laravel-auth0/tree/7.0.0-BETA2) (2022-03-09)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.5.0...7.0.0-BETA2)
-
-Please review the [BETA1 changelog notes below](#700-beta1-2022-02-08) before upgrading your application from 6.x, as 7.0 is a new major containing breaking changes. As with all beta releases, this should not be considered stable or suitable for production use, but your experimentation with and feedback around it is greatly appreciated.
-
-**Changes**
-- Update Middleware interface checks for custom user model types [\#263](https://github.com/auth0/laravel-auth0/pull/263) ([sheggi](https://github.com/sheggi))
-- Updated UserProvider API [\#264](https://github.com/auth0/laravel-auth0/pull/264) ([evansims](https://github.com/evansims))
-- Add Rector to test suite [\#265](https://github.com/auth0/laravel-auth0/pull/265) ([evansims](https://github.com/evansims))
-
-## [7.0.0-BETA1](https://github.com/auth0/laravel-auth0/tree/7.0.0-BETA1) (2022-02-08)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.5.0...7.0.0-BETA1)
-
-Auth0 Laravel SDK v7 includes many significant changes over previous versions:
-
-- Support for Laravel 9.
-- Support for Auth0-PHP SDK 8.
-- New authentication route controllers for plug-and-play login support.
-- Improved authentication middleware for regular web applications.
-- New authorization middleware for token-based backend API applications.
-
-As expected with a major release, Auth0 Laravel SDK v7 includes breaking changes. Please review the [upgrade guide](UPGRADE.md) thoroughly to understand the changes required to migrate your application to v7.
-
-**Breaking Changes Summary**
-
-- Namespace has been updated from `Auth0\Login` to `Auth0\Laravel`
-- Auth0-PHP SDK dependency updated to V8
-- New configuration format
-- SDK now self-registers its services and middleware
-
-## [6.5.0](https://github.com/auth0/laravel-auth0/tree/6.5.0) (2021-10-15)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.4.1...6.5.0)
+## [7.11.0](https://github.com/auth0/laravel-auth0/tree/7.11.0) (2023-08-08)
**Added**
-- Add SDK alias methods for passwordless endpoints [\#228](https://github.com/auth0/laravel-auth0/pull/228) ([evansims](https://github.com/evansims))
-
-## [6.4.1](https://github.com/auth0/laravel-auth0/tree/6.4.0) (2021-08-02)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.4.0...6.4.1)
+- Significant performance improvements by eliminating redundant user queries.
+- Compatibility support for [Laravel Telescope](https://laravel.com/docs/telescope). See [docs/Telescope.md](./docs/Telescope.md) for more information.
+- A refactored Events API has been introduced. See [docs/Events.md](./docs/Events.md) for more information.
+- `AUTH0_SESSION_STORAGE` and `AUTH0_TRANSIENT_STORAGE` now support a `cookie` value to enable the native Auth0-PHP SDK cookie session handler. See [docs/Cookies.md](./docs/Cookies.md) for more information.
**Fixed**
-- Use the fully qualified facade class names [\#215](https://github.com/auth0/laravel-auth0/pull/215) ([Rezouce](https://github.com/Rezouce))
-- Update auth0-PHP dependency [\#222](https://github.com/auth0/laravel-auth0/pull/222) ([evansims](https://github.com/evansims))
-- Pass api_identifier config as audience to Auth0\SDK\Auth0 [\#214](https://github.com/auth0/laravel-auth0/pull/214) ([iSerter](https://github.com/iSerter))
-
-## [6.4.0](https://github.com/auth0/laravel-auth0/tree/6.4.0) (2021-03-25)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.3.0...6.4.0)
+- Addressed an issue where, under certain circumstances, the first user authentication attempt after a session invalidation could fail.
**Changed**
-- Add support for Auth0 Organizations [\#209](https://github.com/auth0/laravel-auth0/pull/209) ([evansims](https://github.com/evansims))
-
-## [6.3.0](https://github.com/auth0/laravel-auth0/tree/6.3.0) (2020-02-18)
+- Session regeneration/invalidation has been refactored.
+- Discarded sessions are now deleted when they are invalidated by the SDK, rather than wait for Laravel to garbage collect.
+- Session storage has been refactored. Session data is now stored as a JSON array in a single `auth0_session` entry in the Laravel session store, rather than in multiple keys.
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.2.0...6.3.0)
+**Documentation**
-**Changed**
+- A demonstration Eloquent user model and repository implementation has been added to [docs/Eloquent.md](./docs/Eloquent.md).
+- A new [docs/Sessions.md](./docs/Sessions.md) document has been added for guidance on the various session driver options available.
-- Store changes made to the user object during the onLogin event hook [\#206](https://github.com/auth0/laravel-auth0/pull/206) ([evansims](https://github.com/evansims))
+## [7.10.1](https://github.com/auth0/laravel-auth0/tree/7.10.1) (2023-08-07)
**Fixed**
-- Avoid throwing an error when calling getUserByUserInfo() during login callback event when the supplied profile is empty/null [\#207](https://github.com/auth0/laravel-auth0/pull/207) ([evansims](https://github.com/evansims))
-
-## [6.2.0](https://github.com/auth0/laravel-auth0/tree/6.2.0) (2020-01-15)
+- Addressed an issue where, under certain circumstances, permissions state could be lost after authenticating.
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.1.0...6.2.0)
+## [7.10.0](https://github.com/auth0/laravel-auth0/tree/7.10.0) (2023-07-24)
**Added**
-- Support PHP 8.0 [\#200](https://github.com/auth0/laravel-auth0/pull/200) ([evansims](https://github.com/evansims))
-
-**Fixed**
-
-- Fix the missing `return null;` in `getUserByIdentifier` [\#201](https://github.com/auth0/laravel-auth0/pull/201) ([sebwas](https://github.com/sebwas))
-
-## [6.1.0](https://github.com/auth0/laravel-auth0/tree/6.1.0) (2020-09-17)
+- Organization Name support added for Authentication API and token handling ¹
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.0.1...6.1.0)
-
-**Added**
+**Changed**
-- Support Laravel 8 [\#190](https://github.com/auth0/laravel-auth0/pull/190) ([giannidhooge](https://github.com/giannidhooge))
+- Guards are now registered with the priority middleware list.
+- Bumped `auth0-php` dependency version range to `^8.7`.
+- Updated telemetry to indicate new `laravel` package name (previously `laravel-auth0`.)
**Fixed**
-- Fix composer.json whitespace issue [\#192](https://github.com/auth0/laravel-auth0/pull/192) ([jimmyjames](https://github.com/jimmyjames))
+- Addressed issue where placeholder `AUTH0_` dotenv values could erroneously be interpreted as true configuration values.
-## [6.0.1](https://github.com/auth0/laravel-auth0/tree/6.0.1) (2020-04-28)
+> **Note**
+> ¹ To use this feature, an Auth0 tenant must have support for it enabled. This feature is not yet available to all tenants.
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/6.0.0...6.0.1)
+## [7.9.1](https://github.com/auth0/laravel-auth0/tree/7.9.1) (2023-06-21)
**Fixed**
-- Fix access token decoding and validation [\#183](https://github.com/auth0/laravel-auth0/pull/183) ([jimmyjames](https://github.com/jimmyjames))
+- Resolved an issue where, under certain circumstances, the AuthenticationGuard middleware could get erroneously added to the `api` middleware group, causing a session to be established in a stateless request. ([\#415](https://github.com/auth0/laravel-auth0/pull/415))
-## [6.0.0](https://github.com/auth0/laravel-auth0/tree/6.0.0) (2020-04-09)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/5.4.0...6.0.0)
-
-**This is a major release and includes breaking changes!** This release also includes a major version change for the PHP SDK that it relies on. Please see the [migration guide](https://github.com/auth0/auth0-PHP/blob/master/MIGRATE-v5-TO-v7.md) for the PHP SDK for more information.
-
-**Closed issues**
-
-- auth0-PHP 7.0 - State and nonce handling [\#163](https://github.com/auth0/laravel-auth0/issues/163)
-- Cannot use actingAs unit tests functionality [\#161](https://github.com/auth0/laravel-auth0/issues/161)
+## [7.9.0](https://github.com/auth0/laravel-auth0/tree/7.9.0) (2023-06-15)
**Added**
-- Implement auth0 guard [\#166](https://github.com/auth0/laravel-auth0/pull/166) ([Tamrael](https://github.com/Tamrael))
-
-**Changed**
-
-- Use array for Auth0JWTUser and add repo return types [\#176](https://github.com/auth0/laravel-auth0/pull/176) ([joshcanhelp](https://github.com/joshcanhelp))
-- Update PHP SDK to v7.0.0 [\#162](https://github.com/auth0/laravel-auth0/pull/162) ([joshcanhelp](https://github.com/joshcanhelp))
-- Bind SessionState handler interface in container [\#147](https://github.com/auth0/laravel-auth0/pull/147) ([nstapelbroek](https://github.com/nstapelbroek))
+- SDK configuration (`config/auth0.php`) now supports a `configurationPath` property for specifying a custom search path for `.auth0.*.json` and `.env*` files. ([\#407](https://github.com/auth0/laravel-auth0/pull/407))
+- `Auth0\Laravel\Guards\GuardAbstract` now extends `Illuminate\Contracts\Auth\Guard`. ([\#410](https://github.com/auth0/laravel-auth0/pull/410))
**Fixed**
-- Fix Laravel session management [\#174](https://github.com/auth0/laravel-auth0/pull/174) ([joshcanhelp](https://github.com/joshcanhelp))
-
-## [5.4.0](https://github.com/auth0/laravel-auth0/tree/5.4.0) (2020-03-27)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/5.3.1...5.4.0)
+- Resolved host environment variables not being loaded as expected when a `.env` file is also used. ([\#408](https://github.com/auth0/laravel-auth0/pull/408))
+- Resolved surrounding quote characters not being trimmed from environment variables and `.env` files during processing. ([\#409](https://github.com/auth0/laravel-auth0/pull/409))
-**Closed issues**
-
-- Laravel 7.0 supported release. [\#171](https://github.com/auth0/laravel-auth0/issues/171)
+## [7.8.1](https://github.com/auth0/laravel-auth0/tree/7.8.1) (2023-05-19)
**Fixed**
-- Fixed PHPDocs [\#170](https://github.com/auth0/laravel-auth0/pull/170) ([YAhiru](https://github.com/YAhiru))
-
-**Added**
-
-- Laravel 7 support [\#167](https://github.com/auth0/laravel-auth0/pull/167) ([giannidhooge](https://github.com/giannidhooge))
-
-## [5.3.1](https://github.com/auth0/laravel-auth0/tree/5.3.1) (2019-11-14)
+- Resolved an issue where parsing `.env` files could sometimes throw an exception when handling non-key-value pair strings. ([\#395](https://github.com/auth0/laravel-auth0/pull/395))
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/5.3.0...5.3.1)
+## [7.8.0](https://github.com/auth0/laravel-auth0/tree/7.8.0) (2023-05-18)
-**Closed issues**
-
-- Setting of state_handler in Auth0Service causes "Invalid state" error [\#154](https://github.com/auth0/laravel-auth0/issues/154)
+**Added**
-**Fixed**
+- This release adds support for authenticating using **[Pushed Authorization Requests](https://www.rfc-editor.org/rfc/rfc6749)**.
-- Allow store and state_handler to be passed in from config [\#156](https://github.com/auth0/laravel-auth0/pull/156) ([joshcanhelp](https://github.com/joshcanhelp))
-- Add 'persist_refresh_token' key to laravel-auth0 configuration file. [\#152](https://github.com/auth0/laravel-auth0/pull/152) ([tpenaranda](https://github.com/tpenaranda))
-- Replace `setEnvironment` with `setEnvProperty` [\#145](https://github.com/auth0/laravel-auth0/pull/145) ([nstapelbroek](https://github.com/nstapelbroek))
+- This release introduces **two new Authentication Guards** which provide a streamlined integration experience for developers that need to simultaneously support both session-based authentication and token-based endpoint authorization in their Laravel applications.
-## [5.3.0](https://github.com/auth0/laravel-auth0/tree/5.3.0) (2019-09-26)
+ | Guard | Class | Description |
+ | --------------------- | ----------------------------------------------- | ----------------------------- |
+ | `auth0.authenticator` | `Auth0\Laravel\Auth\Guards\AuthenticationGuard` | Session-based authentication. |
+ | `auth0.authorizer` | `Auth0\Laravel\Auth\Guards\AuthorizationGuard` | Token-based authorization. |
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/5.2.0...5.3.0)
+- These guards are compatible with Laravel's Authentication API and support the standard `auth` middleware.
-**Closed issues**
+- These guards are compatible with Laravel's Authorization API and support the standard `can` middleware, and the `Guard` facade, and work with the Policies API.
-- Feature request: Add Laravel 6 support [\#138](https://github.com/auth0/laravel-auth0/issues/138)
-- SessionStateHandler should use LaravelSessionStore not SessionStore [\#125](https://github.com/auth0/laravel-auth0/issues/125)
+- 3 new pre-built Guards are available: `scope` and `permission`, as well as a dynamic `*:*`. This enables you to verify whether the user's access token has a particular scope or (if RBAC is enabled on the Auth0 API) a particular permission. For example `Gate::check('scope', 'email')` or `Route::get(/*...*/)->can('read:messages')`.
-**Added**
+- The SDK now automatically registers these guards to Laravel's standard `web` and `api` middleware groups, respectively. Manual Guard setup in `config/auth.php` is no longer necessary.
-- Support Laravel 6 [\#139](https://github.com/auth0/laravel-auth0/pull/139) ([FreekVR](https://github.com/FreekVR))
+- The SDK now automatically registers the Authentication routes. Manual route setup in `routes/web.php` is no longer necessary.
-**Fixed**
+- 2 new routing Middleware have been added: `Auth0\Laravel\Http\Middleware\AuthenticatorMiddleware` and `Auth0\Laravel\Http\Middleware\AuthorizerMiddleware`. These are automatically registered with your Laravel application, and ensure the Auth0 Guards are used for authentication for `web` routes and authorization for `api` routes, respectively. This replaces the need for the `guard` middleware or otherwise manual Guard assignment in your routes.
-- Use LaravelSessionStore in the SessionStateHandler. [\#135](https://github.com/auth0/laravel-auth0/pull/135) ([nstapelbroek](https://github.com/nstapelbroek))
+**Changed**
-## [5.2.0](https://github.com/auth0/laravel-auth0/tree/5.2.0) (2019-06-27)
+- We've introduced **a new configuration syntax**. This new syntax is more flexible and allows for more complex configuration scenarios, and introduces support for multiple guard instances. Developers using the previous syntax will have their existing configurations applied to all guards uniformly.
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/5.1.0...5.2.0)
+- The SDK can now **configure itself using a `.auth0.json` file in the project root directory**. This file can be generated [using the Auth0 CLI](./docs/Configuration.md), and provides a significantly simpler configuration experience for developers.
-**Closed issues**
+- The previous `auth0.guard` Guard (`Auth0\Laravel\Auth\Guard`) has been **refactored** as a lightweight wrapper around the new `AuthenticationGuard` and `AuthorizationGuard` guards.
-- Authenticate as a Laravel API user using the Auth0 token [\#129](https://github.com/auth0/laravel-auth0/issues/129)
-- Redirect to previous page after login [\#122](https://github.com/auth0/laravel-auth0/issues/122)
-- Auth0User uses private variables so they cannot be accessed or overridden in child class [\#120](https://github.com/auth0/laravel-auth0/issues/120)
-- API routes broken in auth0-laravel-php-web-app (and in general)? [\#117](https://github.com/auth0/laravel-auth0/issues/117)
-- API returning "token algorithm not supported" [\#116](https://github.com/auth0/laravel-auth0/issues/116)
-- Changing name of user identifier [\#115](https://github.com/auth0/laravel-auth0/issues/115)
-- Possible to use User object functions? [\#114](https://github.com/auth0/laravel-auth0/issues/114)
-- Auth0-PHP@5.3.1 breaks Laravel-Auth0 [\#108](https://github.com/auth0/laravel-auth0/issues/108)
-- Extend Illuminate\Foundation\Auth\User [\#104](https://github.com/auth0/laravel-auth0/issues/104)
-- [Bug] Inconsistencies with the singleton Auth0Service [\#103](https://github.com/auth0/laravel-auth0/issues/103)
-- How do you combine Auth0 Lock with Laravel Auth0? [\#102](https://github.com/auth0/laravel-auth0/issues/102)
-- OnLogin callback question [\#97](https://github.com/auth0/laravel-auth0/issues/97)
+## [7.7.0](https://github.com/auth0/laravel-auth0/tree/7.7.0) (2023-04-26)
**Added**
-- Add composer.lock file [\#123](https://github.com/auth0/laravel-auth0/pull/123) ([lbalmaceda](https://github.com/lbalmaceda))
+- `Auth0\Laravel\Auth0` now has a `management()` shortcut method for issuing Management API calls. ([\#376](https://github.com/auth0/laravel-auth0/pull/376))
-**Changed**
-
-- Change private properties to protected [\#132](https://github.com/auth0/laravel-auth0/pull/132) ([joshcanhelp](https://github.com/joshcanhelp))
-- Return null instead of false in Auth0UserProvider. [\#128](https://github.com/auth0/laravel-auth0/pull/128) ([afreakk](https://github.com/afreakk))
-- Change the visibility of the getter method from private to public [\#121](https://github.com/auth0/laravel-auth0/pull/121) ([irieznykov](https://github.com/irieznykov))
-- Updated required PHP version to 5.4 in composer [\#118](https://github.com/auth0/laravel-auth0/pull/118) ([dmyers](https://github.com/dmyers))
-- Changed arrays to use short array syntax [\#110](https://github.com/auth0/laravel-auth0/pull/110) ([dmyers](https://github.com/dmyers))
+- `Auth0\Laravel\Auth0\Guard` now has a `refreshUser()` method for querying `/userinfo` endpoint and refreshing the authenticated user's cached profile data. ([\#375](https://github.com/auth0/laravel-auth0/pull/375))
-**Fixed**
+- `Auth0\Laravel\Http\Controller\Stateful\Login` now raises a `LoginAttempting` event, offering an opportunity to customize the authorization parameters before the login redirect is issued. ([\#382](https://github.com/auth0/laravel-auth0/pull/382))
-- Fix cachehandler resolving issues [\#131](https://github.com/auth0/laravel-auth0/pull/131) ([deviouspk](https://github.com/deviouspk))
-- Added the Auth0Service as a singleton through the classname [\#107](https://github.com/auth0/laravel-auth0/pull/107) ([JCombee](https://github.com/JCombee))
-- Fixed typo [\#106](https://github.com/auth0/laravel-auth0/pull/106) ([IvanArjona](https://github.com/IvanArjona))
-
-## [5.1.0](https://github.com/auth0/laravel-auth0/tree/5.1.0) (2018-03-20)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/5.0.2...5.1.0)
+**Changed**
-**Closed issues**
+- The `tokenCache`, `managementTokenCache`, `sessionStorage` and `transientStorage` configuration values now support `false` or `string` values pointing to class names (e.g. `\Some\Cache::class`) or class aliases (e.g. `cache.psr6`) registered with Laravel. ([\#381](https://github.com/auth0/laravel-auth0/pull/381))
-- pls change config arg name [\#95](https://github.com/auth0/laravel-auth0/issues/95)
+## [7.6.0](https://github.com/auth0/laravel-auth0/tree/7.6.0) (2023-04-12)
**Added**
-- AutoDiscovery [\#91](https://github.com/auth0/laravel-auth0/pull/91) ([m1guelpf](https://github.com/m1guelpf))
-- Added guzzle options to config to allow for connection options [\#88](https://github.com/auth0/laravel-auth0/pull/88) ([mjmgooch](https://github.com/mjmgooch))
+- `Auth0\Laravel\Http\Middleware\Guard`, new middleware that forces Laravel to route requests through a group using a specific Guard. ([\#362](https://github.com/auth0/laravel-auth0/pull/362))
**Changed**
-- Change default settings file [\#96](https://github.com/auth0/laravel-auth0/pull/96) ([joshcanhelp](https://github.com/joshcanhelp))
-- Utilise Auth0->Login to ensure state validation [\#90](https://github.com/auth0/laravel-auth0/pull/90) ([cocojoe](https://github.com/cocojoe))
+- `Auth0\Laravel\Http\Middleware\Stateful\Authenticate` now remembers the intended route (using `redirect()->setIntendedUrl()`) before kicking off the authentication flow redirect. Users will be returned to the memorized intended route after completing their authentication flow. ([\#364](https://github.com/auth0/laravel-auth0/pull/364))
**Fixed**
-- Make code comments gender neutral [\#98](https://github.com/auth0/laravel-auth0/pull/98) ([devjack](https://github.com/devjack))
-- Fix README and CHANGELOG [\#99](https://github.com/auth0/laravel-auth0/pull/99) ([joshcanhelp](https://github.com/joshcanhelp))
-
-## [5.0.2](https://github.com/auth0/laravel-auth0/tree/5.0.2) (2017-08-30)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/5.0.1...5.0.2)
-
-**Merged pull requests:**
-
-- Use instead of to identify the Auth0 user [\#80](https://github.com/auth0/laravel-auth0/pull/80) ([glena](https://github.com/glena))
-
-## [5.0.1](https://github.com/auth0/laravel-auth0/tree/5.0.1) (2017-02-23)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/5.0.0...5.0.1)
-
-Fixed `supported_algs` configuration name
-
-## [5.0.0](https://github.com/auth0/laravel-auth0/tree/5.0.0) (2017-02-22)
+- legacyGuardUserMethod behavior should use `$session`, not `$token` ([\#353](https://github.com/auth0/laravel-auth0/pull/365))
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/4.0.8...5.0.0)
+## [7.5.2](https://github.com/auth0/laravel-auth0/tree/7.5.2) (2023-04-10)
-**Merged pull requests:**
-
-- V5: update to auth0 sdk v5 [\#69](https://github.com/auth0/laravel-auth0/pull/69) ([glena](https://github.com/glena))
-
-## [4.0.8](https://github.com/auth0/laravel-auth0/tree/4.0.8) (2017-01-27)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/4.0.7...4.0.8)
-
-**Closed issues**
-
-- Allow use of RS256 Protocol [\#63](https://github.com/auth0/wp-auth0/issues/63)
-- Add RS256 to the list of supported algorithms [\#62](https://github.com/auth0/wp-auth0/issues/62)
-
-**Merged pull requests:**
-
-- allow to configure the algorithm supported for token verification [\#65](https://github.com/auth0/laravel-auth0/pull/65) ([glena](https://github.com/glena))
-
-## [4.0.7](https://github.com/auth0/laravel-auth0/tree/4.0.7) (2017-01-02)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/4.0.6...4.0.7)
-
-**Merged pull requests:**
-
-- it should pass all the configs to the oauth client [\#64](https://github.com/auth0/laravel-auth0/pull/64) ([glena](https://github.com/glena))
-
-## [4.0.6](https://github.com/auth0/laravel-auth0/tree/4.0.6) (2016-11-29)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/4.0.5...4.0.6)
-
-**Merged pull requests:**
-
-- Code style & docblocks [\#56](https://github.com/auth0/laravel-auth0/pull/56) ([seanmangar](https://github.com/seanmangar))
-- Adding accessor to retrieve JWT from Auth0Service [\#58](https://github.com/auth0/laravel-auth0/pull/58) ([ryantology](https://github.com/ryantology))
-
-## [4.0.5](https://github.com/auth0/laravel-auth0/tree/4.0.5) (2016-11-29)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/4.0.4...4.0.5)
-
-**Merged pull requests:**
-
-- Added flag for not encoded tokens + removed example [\#57](https://github.com/auth0/laravel-auth0/pull/57) ([glena](https://github.com/glena))
-
-## [4.0.4](https://github.com/auth0/laravel-auth0/tree/4.0.4) (2016-11-25)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/4.0.2...4.0.4)
-
-**Merged pull requests:**
-
-- Fixing config type [\#55](https://github.com/auth0/laravel-auth0/pull/55) ([adamgoose](https://github.com/adamgoose))
-
-## [4.0.2](https://github.com/auth0/laravel-auth0/tree/4.0.2) (2016-10-03)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/4.0.1...4.0.2)
-
-**Merged pull requests:**
-
-- Fixing JWTVerifier [\#54](https://github.com/auth0/laravel-auth0/pull/54) ([adamgoose](https://github.com/adamgoose))
-
-## [4.0.1](https://github.com/auth0/laravel-auth0/tree/4.0.1) (2016-09-19)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/4.0.0...4.0.1)
-
-**Merged pull requests:**
-
-- fix error becuase of contract and class with the same name [\#52](https://github.com/auth0/laravel-auth0/pull/52) ([glena](https://github.com/glena))
-
-## [4.0.0](https://github.com/auth0/laravel-auth0/tree/4.0.0) (2016-09-15)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/3.2.1...4.0.0)
-
-Better support for Laravel 5.3: Support for Laravel Passport for token verification
-Support of auth0 PHP sdk v4 with JWKs cache
-
-**Merged pull requests:**
-
-- Merge pull request #50 from auth0/4.x.x-dev [\#50](https://github.com/auth0/laravel-auth0/pull/50) ([glena](https://github.com/glena))
-
-## [3.2.1](https://github.com/auth0/laravel-auth0/tree/3.2.1) (2016-09-12)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/3.2.0...3.2.1)
-
-**Merged pull requests:**
-
-- Fix for Laravel 5.2 [\#49](https://github.com/auth0/laravel-auth0/pull/49) ([dscafati](https://github.com/dscafati))
-
-## [3.2.0](https://github.com/auth0/laravel-auth0/tree/3.2.0) (2016-07-11)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/3.1.0...3.2.0)
-
-**Merged pull requests:**
-
-- New optional jwt middleware [\#40](https://github.com/auth0/laravel-auth0/pull/40) ([glena](https://github.com/glena))
-
-## [3.1.0](https://github.com/auth0/laravel-auth0/tree/3.1.0) (2016-05-02)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/3.0.3...3.1.0)
-
-**Merged pull requests:**
-
-- 3.1.0 [\#36](https://github.com/auth0/laravel-auth0/pull/36) ([glena](https://github.com/glena))
-
-## [3.0.3](https://github.com/auth0/laravel-auth0/tree/3.0.3) (2016-01-28)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/3.0.2...3.0.3)
-
-**Closed issues:**
-
-- Tag 2.2.2 breaks on Laravel 5.1 [\#30](https://github.com/auth0/laravel-auth0/issues/30)
-
-**Merged pull requests:**
-
-- Conform to 5.2's Authenticatable contract [\#31](https://github.com/auth0/laravel-auth0/pull/31) ([ryannjohnson](https://github.com/ryannjohnson))
-
-## [3.0.2](https://github.com/auth0/laravel-auth0/tree/3.0.2) (2016-01-25)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/2.2.1...3.0.2)
-
-**Merged pull requests:**
-
-- Added optional persistence configuration values [\#29](https://github.com/auth0/laravel-auth0/pull/29) ([carnevalle](https://github.com/carnevalle))
-
-## [2.2.1](https://github.com/auth0/laravel-auth0/tree/2.2.1) (2016-01-22)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/3.0.1...2.2.1)
-
-**Closed issues:**
-
-- Create a logout route [\#25](https://github.com/auth0/laravel-auth0/issues/25)
-
-**Merged pull requests:**
-
-- Auth0 SDK checks for null values instead of false [\#27](https://github.com/auth0/laravel-auth0/pull/27) ([thijsvdanker](https://github.com/thijsvdanker))
-
-## [3.0.1](https://github.com/auth0/laravel-auth0/tree/3.0.1) (2016-01-18)
-
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/3.0.0...3.0.1)
-
-**Merged pull requests:**
-
-- updated auth0-php dependency [\#24](https://github.com/auth0/laravel-auth0/pull/24) ([glena](https://github.com/glena))
+**Fixed**
-## [3.0.0](https://github.com/auth0/laravel-auth0/tree/3.0.0) (2016-01-06)
+- Relaxed response types from middleware to use low-level `Symfony\Component\HttpFoundation\Response` class, allowing for broader and custom response types.
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/2.2.0...3.0.0)
+## [7.5.1](https://github.com/auth0/laravel-auth0/tree/7.5.1) (2023-04-04)
-**Closed issues:**
+**Fixed**
-- auth0/auth0-php ~1.0 requirement doesn't support latest GuzzleHttp [\#21](https://github.com/auth0/laravel-auth0/issues/21)
+- Resolved an issue wherein custom user repositories could fail to be instantiated under certain circumstances.
-**Merged pull requests:**
+## [7.5.0](https://github.com/auth0/laravel-auth0/tree/7.5.0) (2023-04-03)
-- updated to be compatible with laravel 5.2 [\#23](https://github.com/auth0/laravel-auth0/pull/23) ([glena](https://github.com/glena))
+This release includes support for Laravel 10, and major improvements to the internal state handling mechanisms of the SDK.
-## [2.2.0](https://github.com/auth0/laravel-auth0/tree/2.2.0) (2015-11-30)
+**Added**
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/2.1.4...2.2.0)
+- Support for Laravel 10 [#349](https://github.com/auth0/laravel-auth0/pull/349)
+- New `Auth0\Laravel\Traits\Imposter` trait to allow for easier testing. [Example usage](./tests/Unit/Traits/ImpersonateTest.php)
+- New Exception types have been added for more precise error-catching.
-**Merged pull requests:**
+**Changed**
-- updated auth0-php dependency version [\#22](https://github.com/auth0/laravel-auth0/pull/22) ([glena](https://github.com/glena))
-- Update login.blade.php [\#20](https://github.com/auth0/laravel-auth0/pull/20) ([Annyv2](https://github.com/Annyv2))
+The following changes have no effect on the external API of this package but may affect internal usage.
-## [2.1.4](https://github.com/auth0/laravel-auth0/tree/2.1.4) (2015-10-27)
+- `Guard` will now more reliably detect changes in the underlying Auth0-PHP SDK session state.
+- `Guard` will now more reliably sync changes back to the underlying Auth0-PHP SDK session state.
+- `StateInstance` concept has been replaced by a new `Credentials` entity.
+- `Guard` updated to use new `Credentials` entity as primary internal storage for user data.
+- `Auth0\Laravel\Traits\ActingAsAuth0User` was updated to use new `Credentials` entity.
+- The HTTP middleware has been refactored to more clearly differentiate between token and session-based identities.
+- The `authenticate`, `authenticate.optional` and `authorize.optional` HTTP middleware now supports scope filtering, as `authorize` already did.
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/2.1.3...2.1.4)
+- Upgraded test suite to use PEST 2.0 framework.
+- Updated test coverage to 100%.
-**Merged pull requests:**
+**Fixed**
-- Middleware contract has been deprecated in 5.1 [\#19](https://github.com/auth0/laravel-auth0/pull/19) ([thijsvdanker](https://github.com/thijsvdanker))
-- Fixed some typo's in the comments. [\#18](https://github.com/auth0/laravel-auth0/pull/18) ([thijsvdanker](https://github.com/thijsvdanker))
-- Removed note about unstable dependency from README [\#17](https://github.com/auth0/laravel-auth0/pull/17) ([thijsvdanker](https://github.com/thijsvdanker))
-- Update composer instructions [\#16](https://github.com/auth0/laravel-auth0/pull/16) ([iWader](https://github.com/iWader))
-- Use a tagged release of adoy/oauth2 [\#15](https://github.com/auth0/laravel-auth0/pull/15) ([thijsvdanker](https://github.com/thijsvdanker))
+- A 'Session store not set on request' error could occur when downstream applications implemented unit testing that uses the Guard. This should be resolved now.
+- `Guard` would not always honor the `provider` configuration value in `config/auth.php`.
+- `Guard` is no longer defined as a Singleton to better support applications that need multi-guard configurations.
-## [2.1.3](https://github.com/auth0/laravel-auth0/tree/2.1.3) (2015-07-17)
+### Notes
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/2.1.2...2.1.3)
+#### Changes to `user()` behavior
-**Merged pull requests:**
+This release includes a significant behavior change around the `user()` method of the Guard. Previously, by simply invoking the method, the SDK would search for any available credential (access token, device session, etc.) and automatically assign the user within the Guard. The HTTP middleware has been upgraded to handle the user assignment step, and `user()` now only returns the current state of the user assignment without altering it.
-- updated jwt dependency [\#14](https://github.com/auth0/laravel-auth0/pull/14) ([glena](https://github.com/glena))
+A new property has been added to the `config/auth0.php` configuration file: `behavior`. This is an array. At this time, there is a single option: `legacyGuardUserMethod`, a bool. If this value is set to true, or if the key is missing, the previously expected behavior will be applied, and `user()` will behave as it did before this release. The property defaults to `false`.
-## [2.1.2](https://github.com/auth0/laravel-auth0/tree/2.1.2) (2015-05-15)
+#### Changes to Guard and Provider driver aliases
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/2.1.1...2.1.2)
+We identified an issue with using identical alias naming for both the Guard and Provider singletons under Laravel 10, which has required us to rename these aliases. As previous guidance had been to instantiate these using their class names, this should not be a breaking change in most cases. However, if you had used `auth0` as the name for either the Guard or the Provider drivers, kindly note that these have changed. Please use `auth0.guard` for the Guard driver and `auth0.provider`` for the Provider driver. This is a regrettable change but was necessary for adequate Laravel 10 support.
-**Merged pull requests:**
+## [7.4.0](https://github.com/auth0/laravel-auth0/tree/7.4.0) (2022-12-12)
-- Added override of info headers [\#13](https://github.com/auth0/laravel-auth0/pull/13) ([glena](https://github.com/glena))
+**Added**
-## [2.1.1](https://github.com/auth0/laravel-auth0/tree/2.1.1) (2015-05-12)
+- feat: Add `Auth0\Laravel\Event\Middleware\...` event hooks [\#340](https://github.com/auth0/laravel-auth0/pull/340)
+- feat: Add `Auth0\Laravel\Event\Configuration\Building` event hook [\#339](https://github.com/auth0/laravel-auth0/pull/339)
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/2.1.0...2.1.1)
+## [7.3.0](https://github.com/auth0/laravel-auth0/tree/7.3.0) (2022-11-07)
-**Closed issues:**
+**Added**
-- SDK Client headers spec compliant [\#11](https://github.com/auth0/laravel-auth0/issues/11)
-- Support for Laravel 5? [\#6](https://github.com/auth0/laravel-auth0/issues/6)
+- add: Raise additional Laravel Auth Events [\#331](https://github.com/auth0/laravel-auth0/pull/331)
-**Merged pull requests:**
+**Fixed**
-- SDK Client headers spec compliant \#11 [\#12](https://github.com/auth0/laravel-auth0/pull/12) ([glena](https://github.com/glena))
+- fix: `env()` incorrectly assigns `cookieExpires` to a `string` value [\#332](https://github.com/auth0/laravel-auth0/pull/332)
+- fix: Auth0\Laravel\Cache\LaravelCachePool::createItem returning a cache miss [\#329](https://github.com/auth0/laravel-auth0/pull/329)
-## [2.1.0](https://github.com/auth0/laravel-auth0/tree/2.1.0) (2015-05-07)
+## [7.2.2](https://github.com/auth0/laravel-auth0/tree/7.2.2) (2022-10-19)
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/2.0.0...2.1.0)
+**Fixed**
-**Merged pull requests:**
+- Restore `php artisan vendor:publish` command [\#321](https://github.com/auth0/laravel-auth0/pull/321)
+- Bump minimum `auth0/auth0-php` version to `^8.3.4` [\#322](https://github.com/auth0/laravel-auth0/pull/322)
-- Upgrade to auth-php 1.0.0: Added support to API V2 [\#10](https://github.com/auth0/laravel-auth0/pull/10) ([glena](https://github.com/glena))
+## [7.2.1](https://github.com/auth0/laravel-auth0/tree/7.2.1) (2022-10-13)
-## [2.0.0](https://github.com/auth0/laravel-auth0/tree/2.0.0) (2015-04-20)
+**Fixed**
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.8...2.0.0)
+- `Auth0\Laravel\Auth0` no longer requires a session configuration for stateless strategies, restoring previous behavior. [\#317](https://github.com/auth0/laravel-auth0/pull/317)
+- The SDK now requires `^3.0` of the `psr/cache` dependency, to accommodate breaking changes made in the upstream interface (typed parameters and return types) for PHP 8.0+. [\#316](https://github.com/auth0/laravel-auth0/pull/316)
-**Merged pull requests:**
+## [7.2.0](https://github.com/auth0/laravel-auth0/tree/7.2.0) (2022-10-10)
-- Package V2 for Laravel5 [\#9](https://github.com/auth0/laravel-auth0/pull/9) ([glena](https://github.com/glena))
+**Changed**
-## [1.0.8](https://github.com/auth0/laravel-auth0/tree/1.0.8) (2015-04-14)
+- `Auth0\Laravel\Store\LaravelSession` has been added as the default `sessionStorage` and `transientStorage` interfaces for the underlying [Auth0-PHP SDK](https://github.com/auth0/auth0-PHP/). The SDK now leverages the native [Laravel Session APIs](https://laravel.com/docs/9.x/session) by default. [\#307](https://github.com/auth0/laravel-auth0/pull/307)¹
+- `Auth0\Laravel\Cache\LaravelCachePool` and `Auth0\Laravel\Cache\LaravelCacheItem` have been added as the default `tokenCache` and `managementTokenCache` interfaces for the underlying [Auth0-PHP SDK](https://github.com/auth0/auth0-PHP/). The SDK now leverages the native [Laravel Cache APIs](https://laravel.com/docs/9.x/cache) by default. [\#307](https://github.com/auth0/laravel-auth0/pull/307)
+- `Auth0\Laravel\Auth\Guard` now supports the `viaRemember` method. [\#306](https://github.com/auth0/laravel-auth0/pull/306)
+- `Auth0\Laravel\Http\Middleware\Stateless\Authorize` now returns a 401 status instead of 403 for unauthenticated users. [\#304](https://github.com/auth0/laravel-auth0/issues/304)
+- PHP 8.0 is now the minimum supported runtime version. Please review the [README](README.md) for more information on support windows.
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.7...1.0.8)
+¹ This change may require your application's users to re-authenticate. You can avoid this by changing the `sessionStorage` and `transientStorage` options in your SDK configuration to their previous default instances of `Auth0\SDK\Store\CookieStore`, but it is recommended you migrate to the new `LaravelSession` default.
-## [1.0.7](https://github.com/auth0/laravel-auth0/tree/1.0.7) (2015-04-13)
+## [7.1.0](https://github.com/auth0/laravel-auth0/tree/7.1.0) (2022-08-08)
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.6...1.0.7)
+**Changed**
-**Merged pull requests:**
+- Return interfaces instead of concrete classes [\#296](https://github.com/auth0/laravel-auth0/pull/296)
+- change: Use class names for `app()` calls [\#291](https://github.com/auth0/laravel-auth0/pull/291)
-- Fixed the way the access token is pased to the A0User [\#7](https://github.com/auth0/laravel-auth0/pull/7) ([glena](https://github.com/glena))
-- Update README.md [\#5](https://github.com/auth0/laravel-auth0/pull/5) ([pose](https://github.com/pose))
+**Fixed**
-## [1.0.6](https://github.com/auth0/laravel-auth0/tree/1.0.6) (2014-08-01)
+- Fix: `Missing Code` error on Callback Route for Octane Customers [\#297](https://github.com/auth0/laravel-auth0/pull/297)
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.5...1.0.6)
+## [7.0.1](https://github.com/auth0/laravel-auth0/tree/7.0.1) (2022-06-01)
-## [1.0.5](https://github.com/auth0/laravel-auth0/tree/1.0.5) (2014-08-01)
+**Fixed**
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.4...1.0.5)
+- Fixed an issue in `Auth0\Laravel\Http\Controller\Stateful\Callback` where `$errorDescription`'s value was assigned an incorrect value when an error was encountered. [\#266](https://github.com/auth0/laravel-auth0/pull/288)
-**Closed issues:**
+## [7.0.0](https://github.com/auth0/laravel-auth0/tree/7.0.0) (2022-03-21)
-- Problem with normal laravel user table [\#4](https://github.com/auth0/laravel-auth0/issues/4)
+Auth0 Laravel SDK v7 includes many significant changes over previous versions:
-**Merged pull requests:**
+- Support for Laravel 9.
+- Support for Auth0-PHP SDK 8.
+- New authentication route controllers for plug-and-play login support.
+- Improved authentication middleware for regular web applications.
+- New authorization middleware for token-based backend API applications.
-- Update README.md [\#3](https://github.com/auth0/laravel-auth0/pull/3) ([patekuru](https://github.com/patekuru))
+As expected with a major release, Auth0 Laravel SDK v7 includes breaking changes. Please review the [upgrade guide](UPGRADE.md) thoroughly to understand the changes required to migrate your application to v7.
-## [1.0.4](https://github.com/auth0/laravel-auth0/tree/1.0.4) (2014-05-07)
+### Breaking Changes
-[Full Changelog](https://github.com/auth0/laravel-auth0/compare/1.0.3...1.0.4)
+- Namespace has been updated from `Auth0\Login` to `Auth0\Laravel`
+- Auth0-PHP SDK dependency updated to V8
+- New configuration format
+- SDK now self-registers its services and middleware
+- New UserProvider API
-## [1.0.3](https://github.com/auth0/laravel-auth0/tree/1.0.3) (2014-04-21)
+> Changelog entries for releases prior to 8.0 have been relocated to [CHANGELOG.ARCHIVE.md](CHANGELOG.ARCHIVE.md).
diff --git a/EXAMPLES.md b/EXAMPLES.md
deleted file mode 100644
index 0ffbc2f3..00000000
--- a/EXAMPLES.md
+++ /dev/null
@@ -1,144 +0,0 @@
-# Examples using laravel-auth0
-
-- [Custom user models and repositories](#custom-user-models-and-repositories)
-- [Authorizing HTTP tests](#authorizing-http-tests)
-
-## Custom user models and repositories
-
-In Laravel, a User Repository is an interface that sits between your authentication source (Auth0) and core Laravel authentication services. It allows you to shape and manipulate the user model and it's data as you need to.
-
-For example, Auth0's unique identifier is a `string` in the format `auth0|123456abcdef`. If you were to attempt to persist a user to many traditional databases you'd likely encounter an error as, by default, a unique identifier is often exected to be an `integer` rather than a `string` type. A custom user model and repository is a great way to address integration challenges like this.
-
-### Creating a custom user model
-
-Let's setup a custom user model for our application. To do this, let's create a file at `app/Auth/Models/User.php` within our Laravel project. This new class needs to implement the `Illuminate\Contracts\Auth\Authenticatable` interface to be compatible with Laravel's Guard API and this SDK. It must also implement either `Auth0\Laravel\Contract\Model\Stateful\User` or `Auth0\Laravel\Contract\Model\Stateless\User` depending on your application's needs. For example:
-
-```php
-
- */
- protected $fillable = [
- 'id',
- 'name',
- 'email',
- ];
-
- /**
- * The attributes that should be hidden for serialization.
- *
- * @var array
- */
- protected $hidden = [];
-
- /**
- * The attributes that should be cast.
- *
- * @var array
- */
- protected $casts = [];
-}
-```
-
-### Creating a Custom User Repository
-
-Now let's create a custom user repository for your application which will return the new new custom model. To do this, create the file `app/Auth/CustomUserRepository.php`. This new class must implment the `Auth0\Laravel\Contract\Auth\User\Repository` interface. This new repository takes in user data returned from Auth0's API, applies it to the `App\Models\User` custom user model created in the previous step, and returns it for use throughout your application.
-
-```php
- 'just_a_random_example|' . $user['sub'] ?? $user['user_id'] ?? null,
- 'name' => $user['name'],
- 'email' => $user['email']
- ]);
- }
-
- public function fromAccessToken(
- array $user
- ): ?\Illuminate\Contracts\Auth\Authenticatable {
- // Simliar to above. Used for stateless application types.
- return null;
- }
-}
-```
-
-### Using a Custom User Repository
-
-Finally, update your application's `config/auth.php` file. Within the Auth0 provider, assign a custom `repository` value pointing to your new custom user provider class. For example:
-
-```php
- 'providers' => [
- //...
-
- 'auth0' => [
- 'driver' => 'auth0',
- 'repository' => App\Auth\CustomUserRepository::class
- ],
- ],
-```
-
-## Authorizing HTTP tests
-
-If your application does contain HTTP tests which access routes that are protected by the `auth0.authorize` middleware, you can use the trait `Auth0\Laravel\Traits\ActingAsAuth0User` in your tests, which will give you a helper method `actingAsAuth0User(array $attributes=[])` simmilar to Laravels `actingAs` method, that allows you to fake beeing authenticated as a Auth0 user.
-
-The argument `attributes` is optional and you can use it to set any auth0 specific user attributes like scope, sub, azp, iap and so on. If no attributes are set, some default values are used.
-
-### Example with a scope protected route
-
-Let's assume you have a route like the following, that is protected by the scope `read:messages`:
-```php
-Route::get('/api/private-scoped', function () {
- return response()->json([
- 'message' => 'Hello from a private endpoint!',
- 'authorized' => Auth::check(),
- 'user' => Auth::check() ? json_decode(json_encode((array) Auth::user(), JSON_THROW_ON_ERROR), true) : null,
- ], 200, [], JSON_PRETTY_PRINT);
-})->middleware(['auth0.authorize:read:messages']);
-```
-
-To be able to test the route from above, the implementation of your test would have to look like this:
-```php
-use Auth0\Laravel\Traits\ActingAsAuth0User;
-
-public function test_readMessages(){
- $response = $this->actingAsAuth0User([
- "scope"=>"read:messages"
- ])->getJson("/api/private-scoped");
-
- $response->assertStatus(200);
-}
-```
diff --git a/LICENSE.txt b/LICENSE.md
similarity index 93%
rename from LICENSE.txt
rename to LICENSE.md
index a77a3309..3cd3cbfa 100644
--- a/LICENSE.txt
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2022 Auth0, Inc. (http://auth0.com)
+Copyright (c) 2023 Auth0, Inc. (https://auth0.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index efbea3b1..c25f3bc1 100644
--- a/README.md
+++ b/README.md
@@ -1,252 +1,438 @@
-![laravel-auth0](https://cdn.auth0.com/website/sdks/banners/auth0-php-banner.png)
+![Auth0 Laravel SDK](https://cdn.auth0.com/website/sdks/banners/laravel-auth0-banner.png)
-Laravel SDK for [Auth0](https://auth0.com) Authentication and Management APIs.
+
+
+
+
+
+
+
+
-[![Package](https://img.shields.io/packagist/dt/auth0/login)](https://packagist.org/packages/auth0/laravel-auth0)
-[![Build](https://img.shields.io/github/workflow/status/auth0/laravel-auth0/Checks)](https://github.com/auth0/laravel-auth0/actions/workflows/checks.yml?query=branch%3Amain)
-[![License](https://img.shields.io/packagist/l/auth0/login)](https://doge.mit-license.org/)
+**The Auth0 Laravel SDK is a PHP package that integrates [Auth0](https://auth0.com) into your Laravel application.** It includes no-code user authentication, extensive Management API support, permissions-based routing access control, and more.
-:books: [Documentation](#documentation) - :rocket: [Getting Started](#getting-started) - :speech_balloon: [Feedback](#feedback)
+- [Requirements](#requirements)
+- [Getting Started](#getting-started)
+ - [1. Install the SDK](#1-install-the-sdk)
+ - [2. Install the CLI](#2-install-the-cli)
+ - [3. Configure the SDK](#3-configure-the-sdk)
+ - [4. Run the Application](#4-run-the-application)
+- [Documentation](#documentation)
+- [QuickStarts](#quickstarts)
+- [Contributing](#contributing)
+- [Code of Conduct](#code-of-conduct)
+- [Security](#security)
+- [License](#license)
-## Documentation
+## Requirements
+
+Your application must use a [supported Laravel version](#supported-laravel-releases), and your host environment must be running a [maintained PHP version](https://www.php.net/supported-versions.php). Please review [our support policy](./docs/Support.md) for more information.
+
+You will also need [Composer](https://getcomposer.org/) and an [Auth0 account](https://auth0.com/signup).
+
+### Supported Laravel Releases
+
+The next major release of Laravel is forecasted for Q1 2025. We anticipate supporting it upon release.
+
+| Laravel | SDK | PHP | Supported Until |
+| ---------------------------------------------- | ----- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ |
+| [11.x](https://laravel.com/docs/11.x/releases) | 7.13+ | [8.3](https://www.php.net/releases/8.3/en.php) | Approx. [March 2026](https://laravel.com/docs/11.x/releases#support-policy) (EOL for Laravel 11) |
+| | | [8.2](https://www.php.net/releases/8.2/en.php) | Approx. [Dec 2025](https://www.php.net/supported-versions.php) (EOL for PHP 8.2) |
+
+We strive to support all actively maintained Laravel releases, prioritizing support for the latest major version with our SDK. If a new Laravel major introduces breaking changes, we may have to end support for past Laravel versions earlier than planned.
+
+Affected Laravel versions will still receive security fixes until their end-of-life date, as announced in our release notes.
+
+### Maintenance Releases
+
+The following releases are no longer being updated with new features by Auth0, but will continue to receive security updates through their end-of-life date.
+
+| Laravel | SDK | PHP | Security Fixes Until |
+| ---------------------------------------------- | ---------- | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
+| [10.x](https://laravel.com/docs/10.x/releases) | 7.5 - 7.12 | [8.3](https://www.php.net/releases/8.3/en.php) | [Feb 2025](https://laravel.com/docs/10.x/releases#support-policy) (EOL for Laravel 10) |
+| | | [8.2](https://www.php.net/releases/8.2/en.php) | [Feb 2025](https://laravel.com/docs/10.x/releases#support-policy) (EOL for Laravel 10) |
+| | | [8.1](https://www.php.net/releases/8.2/en.php) | [Nov 2024](https://www.php.net/supported-versions.php) (EOL for PHP 8.1) |
+
+### Unsupported Releases
-- Stateful Applications
- - [Quickstart](https://auth0.com/docs/quickstart/webapp/laravel) — add login, logout and user information to a Laravel application using Auth0.
- - [Sample Application](https://github.com/auth0-samples/auth0-laravel-php-web-app) — a sample Laravel web application integrated with Auth0.
-- Stateless Applications
- - [Quickstart](https://auth0.com/docs/quickstart/backend/php) — add access token handling and route authorization to a backend Laravel application using Auth0.
- - [Sample Application](https://github.com/auth0-samples/auth0-laravel-api-samples) — a sample Laravel backend application integrated with Auth0.
-- [Examples](./EXAMPLES.md) — code samples for common scenarios.
-- [Docs site](https://www.auth0.com/docs) — explore our docs site and learn more about Auth0.
+The following releases are unsupported by Auth0. While they may be suitable for some legacy applications, your mileage may vary. We recommend upgrading to a supported version as soon as possible.
+
+| Laravel | SDK |
+| -------------------------------------------- | ---------- |
+| [9.x](https://laravel.com/docs/9.x/releases) | 7.0 - 7.12 |
+| [8.x](https://laravel.com/docs/8.x/releases) | 7.0 - 7.4 |
+| [7.x](https://laravel.com/docs/7.x/releases) | 5.4 - 6.5 |
+| [6.x](https://laravel.com/docs/6.x/releases) | 5.3 - 6.5 |
+| [5.x](https://laravel.com/docs/5.x/releases) | 2.0 - 6.1 |
+| [4.x](https://laravel.com/docs/4.x/releases) | 1.x |
## Getting Started
-### Requirements
+The following is our recommended approach to getting started with the SDK. Alternatives are available in [our expanded installation guide](./docs/Installation.md).
-- PHP 8.0+
-- Laravel 8 / Laravel 9
+### 1. Install the SDK
-> Please review our [support policy](#support-policy) to learn when language and framework versions will exit support in the future.
+- For **new applications**, we offer a quickstart template — a version of the default Laravel 9 starter project pre-configured for use with the Auth0 SDK.
-> [Octane support](#octane-support) is experimental and not advisable for use in production at this time.
+ ```shell
+ composer create-project auth0-samples/laravel auth0-laravel-app && cd auth0-laravel-app
+ ```
-### Installation
+- For **existing applications**, you can install the SDK using Composer.
-Add the dependency to your application with [Composer](https://getcomposer.org/):
+ ```shell
+ composer require auth0/login:^7 --update-with-all-dependencies
+ ```
-```
-composer require auth0/login
-```
+ In this case, you will also need to generate an SDK configuration file for your application.
-### Configure Auth0
+ ```shell
+ php artisan vendor:publish --tag auth0
+ ```
-Create a **Regular Web Application** in the [Auth0 Dashboard](https://manage.auth0.com/#/applications). Verify that the "Token Endpoint Authentication Method" is set to `POST`.
+
-Next, configure the callback and logout URLs for your application under the "Application URIs" section of the "Settings" page:
+### 2. Install the CLI
-- **Allowed Callback URLs**: The URL of your application where Auth0 will redirect to during authentication, e.g., `http://localhost:3000/callback`.
-- **Allowed Logout URLs**: The URL of your application where Auth0 will redirect to after user logout, e.g., `http://localhost:3000/login`.
+1. Install the [Auth0 CLI](https://github.com/auth0/auth0-cli) to manage your account from the command line.
-Note the **Domain**, **Client ID**, and **Client Secret**. These values will be used during configuration later.
+ ```shell
+ curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh -s -- -b .
+ ```
-### Publish SDK configuration
+ Move the CLI to a directory in your `PATH` to make it available system-wide.
-Use Laravel's CLI to generate an Auth0 configuration file within your project:
+ ```shell
+ sudo mv ./auth0 /usr/local/bin
+ ```
-```
-php artisan vendor:publish --tag auto0-config
-```
+
💡 If you prefer not to move the CLI, simply substitute `auth0` in the CLI steps below with `./auth0`.
-A new file will appear within your project, `app/config/auth0.php`. You should avoid making changes to this file directly.
+
+ Using Homebrew (macOS)
+
-### Configure your `.env` file
+ ```shell
+ brew tap auth0/auth0-cli && brew install auth0
+ ```
-Open the `.env` file within your application's directory, and add the following lines appropriate for your application type:
+
-
- For Stateful Web Applications
+
+ Using Scoop (Windows)
+
-```
-AUTH0_DOMAIN="Your Auth0 domain"
-AUTH0_CLIENT_ID="Your Auth0 application client ID"
-AUTH0_CLIENT_SECRET="Your Auth0 application client secret"
-AUTH0_COOKIE_SECRET="A randomly generated string"
+ ```cmd
+ scoop bucket add auth0 https://github.com/auth0/scoop-auth0-cli.git
+ scoop install auth0
+ ```
+
+
+
+2. Authenticate the CLI with your Auth0 account. Choose "as a user" if prompted.
+
+ ```shell
+ auth0 login
+ ```
+
+### 3. Configure the SDK
+
+1. Register a new application with Auth0.
+
+ ```shell
+ auth0 apps create \
+ --name "My Laravel Application" \
+ --type "regular" \
+ --auth-method "post" \
+ --callbacks "http://localhost:8000/callback" \
+ --logout-urls "http://localhost:8000" \
+ --reveal-secrets \
+ --no-input \
+ --json > .auth0.app.json
+ ```
+
+2. Register a new API with Auth0.
+
+ ```shell
+ auth0 apis create \
+ --name "My Laravel Application API" \
+ --identifier "https://github.com/auth0/laravel-auth0" \
+ --offline-access \
+ --no-input \
+ --json > .auth0.api.json
+ ```
+
+3. Add the new files to `.gitignore`.
+
+ ```bash
+ echo ".auth0.*.json" >> .gitignore
+ ```
+
+
+ Using Windows PowerShell
+
+
+ ```powershell
+ Add-Content .gitignore "`n.auth0.*.json"
+ ```
+
+
+
+
+ Using Windows Command Prompt
+
+
+ ```cmd
+ echo .auth0.*.json >> .gitignore
+ ```
+
+
+
+### 4. Run the Application
+
+Boot the application using PHP's built-in web server.
+
+```shell
+php artisan serve
```
-Provide a sufficiently long, random string for your `AUTH0_COOKIE_SECRET` using `openssl rand -hex 32`.
-
+Direct your browser to [http://localhost:8000](http://localhost:8000) to experiment with the application.
+
+- **Authentication**
+ Users can log in or out of the application by visiting the [`/login`](http://localhost:8000/login) or [`/logout`](http://localhost:8000/logout) routes, respectively.
+
+- **API Authorization**
+ For simplicity sake, generate a test token using the CLI.
+
+ ```shell
+ auth0 test token \
+ --audience %IDENTIFIER% \
+ --scopes "read:messages"
+ ```
+
+
✋ Substitute %IDENTIFIER% with the identifier of the API you created in step 3 above.
+
+ Now you can send requests to the `/api` endpoints of the application, including the token as a header.
+
+ ```shell
+ curl --request GET \
+ --url http://localhost:8000/api/example \
+ --header 'Accept: application/json' \
+ --header 'Authorization: Bearer %TOKEN%'
+ ```
+
+
✋ Substitute %TOKEN% with the test token returned in the previous step.
+
+
+ Using Windows PowerShell
+
+
+ ```powershell
+ Invoke-WebRequest http://localhost:8000/api/example `
+ -Headers @{'Accept' = 'application/json'; 'Authorization' = 'Bearer %TOKEN%'}
+ ```
+
+
+
+When you're ready to deploy your application to production, review [our deployment guide](./docs/Deployment.md) for best practices and advice on securing Laravel.
+
+## Integration Examples
- For Stateless Backend Applications
+User Authentication
+
+
+The SDK automatically registers all the necessary routes and authentication services within the `web` middleware group of your application to enable users to authenticate without requiring you to write any code.
+
+| Route | Purpose |
+| ----------- | ---------------------------------- |
+| `/login` | Initiates the authentication flow. |
+| `/logout` | Logs the user out. |
+| `/callback` | Handles the callback from Auth0. |
+
+If these routes conflict with your application architecture, you can override this default behavior by [adjusting the SDK configuration](./docs/Configuration.md#route-registration).
+
+---
-```
-AUTH0_STRATEGY="api"
-AUTH0_DOMAIN="Your Auth0 domain"
-AUTH0_CLIENT_ID="Your Auth0 application client ID"
-AUTH0_CLIENT_SECRET="Your Auth0 application client secret"
-AUTH0_AUDIENCE="Your Auth0 API identifier"
-```
-### Setup your Laravel application
+
+Route Authorization (Access Control)
+
-Integrating the SDK's Guard requires changes to your `config\auth.php` file.
+The SDK automatically registers its authentication and authorization guards within the `web` and `api` middleware groups for your Laravel application, respectively.
-To begin, find the `defaults` section. Set the default `guard` to `auth0`, like this:
+For `web` routes, you can use Laravel's `auth` middleware to require that a user be authenticated to access a route.
```php
-// 📂 config/auth.php
-'defaults' => [
- 'guard' => 'auth0',
- // 📝 Leave any other settings in this section alone.
-],
+Route::get('/private', function () {
+ return response('Welcome! You are logged in.');
+})->middleware('auth');
```
-Next, find the `guards` section, and add `auth0` there:
-```php
-// 👆 Continued from above, in config/auth.php
-'guards' => [
- // 📝 Any additional guards you use should stay here, too.
- 'auth0' => [
- 'driver' => 'auth0',
- 'provider' => 'auth0',
- ],
-],
-```
+For `api` routes, you can use Laravel's `auth` middleware to require that a request be authenticated with a valid bearer token to access a route.
-Finally, find the `providers` section, and add `auth0` there as well:
```php
-// 👆 Continued from above, in config/auth.php
-'providers' => [
- // 📝 Any additional providers you use should stay here, too.
- 'auth0' => [
- 'driver' => 'auth0',
- 'repository' => \Auth0\Laravel\Auth\User\Repository::class
- ],
-],
+Route::get('/api/private', function () {
+ return response()->json(['message' => 'Hello! You included a valid token with your request.']);
+})->middleware('auth');
```
-## Add login to stateful web applications
-
-For regular web applications that provide login and logout, we provide prebuilt route controllers to add to your `app/routes/web.php` file that will automatically handle your application's authentication flow with Auth0 for you:
+In addition to requiring that a user be authenticated, you can also require that the user have specific permissions to access a route, using Laravel's `can` middleware.
```php
-Route::get('/login', \Auth0\Laravel\Http\Controller\Stateful\Login::class)->name('login');
-Route::get('/logout', \Auth0\Laravel\Http\Controller\Stateful\Logout::class)->name('logout');
-Route::get('/auth0/callback', \Auth0\Laravel\Http\Controller\Stateful\Callback::class)->name('auth0.callback');
+Route::get('/scope', function () {
+ return response('You have the `read:messages` permission, and can therefore access this resource.');
+})->middleware('auth')->can('read:messages');
```
-## Protect routes with middleware
+Permissions require that [RBAC](https://auth0.com/docs/manage-users/access-control/rbac) be enabled within [your API settings](https://manage.auth0.com/#/apis).
+
+---
-This SDK includes middleware to simplify either authenticating (regular web applications) or authorizing (backend api applications) your Laravel routes, depending on your application type.
+
-Stateful Web Applications
+Users and Tokens
+
-These are for traditional applications that handle logging in and out.
+Laravel's `Auth` Facade can be used to retrieve information about the authenticated user or token associated with a request.
-The `auth0.authenticate` middleware will check for an available user session and redirect any requests without one to the login route:
+For routes using the `web` middleware group in `routes/web.php`.
```php
-Route::get('/required', function () {
- return view('example.user.template');
-})->middleware(['auth0.authenticate']);
+Route::get('/', function () {
+ if (! auth()->check()) {
+ return response('You are not logged in.');
+ }
+
+ $user = auth()->user();
+ $name = $user->name ?? 'User';
+ $email = $user->email ?? '';
+
+ return response("Hello {$name}! Your email address is {$email}.");
+});
```
-The `auth0.authenticate.optional` middleware will check for an available user session, but won't reject or redirect requests without one, allowing you to treat such requests as "guest" requests:
+For routes using the `api` middleware group in `routes/api.php`.
```php
Route::get('/', function () {
- if (Auth::check()) {
- return view('example.user.template');
- }
-
- return view('example.guest.template');
-})->middleware(['auth0.authenticate.optional']);
+ if (! auth()->check()) {
+ return response()->json([
+ 'message' => 'You did not provide a token.',
+ ]);
+ }
+
+ return response()->json([
+ 'message' => 'Your token is valid; you are authorized.',
+ 'id' => auth()->id(),
+ 'token' => auth()?->user()?->getAttributes(),
+ ]);
+});
```
-> Note that the `example.user.template` and `example.guest.templates` views are just examples and are not part of the SDK; replace these as appropriate for your application.
+---
+
-Stateless Backend Applications
+Management API Calls
+
+
+Once you've [authorized your application to make Management API calls](./docs/Management.md#api-application-authorization), you'll be able to engage nearly any of the [Auth0 Management API endpoints](https://auth0.com/docs/api/management/v2) through the SDK.
-These are applications that accept an a Access Token through the 'Authorization' header of a request.
+Each API endpoint has its own SDK class which can be accessed through the Facade's `management()` factory method. For interoperability, network responses from the API are returned as [PSR-7 messages](https://www.php-fig.org/psr/psr-7/). These can be converted into native arrays using the SDK's `json()` method.
-The `auth0.authorize` middleware will resolve a Access Token and reject any request with an invalid token.
+For example, to update a user's metadata, you can call the `management()->users()->update()` method.
```php
-Route::get('/api/private', function () {
- return response()->json([
- 'message' => 'Hello from a private endpoint! You need to be authenticated to see this.',
- 'authorized' => Auth::check(),
- 'user' => Auth::check() ? json_decode(json_encode((array) Auth::user(), JSON_THROW_ON_ERROR), true) : null,
- ], 200, [], JSON_PRETTY_PRINT);
-})->middleware(['auth0.authorize']);
-```
+use Auth0\Laravel\Facade\Auth0;
-The `auth0.authorize` middleware also allows you to optionally filter requests for access tokens based on scopes:
+Route::get('/colors', function () {
+ $colors = ['red', 'blue', 'green', 'black', 'white', 'yellow', 'purple', 'orange', 'pink', 'brown'];
-```php
-Route::get('/api/private-scoped', function () {
- return response()->json([
- 'message' => 'Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.',
- 'authorized' => Auth::check(),
- 'user' => Auth::check() ? json_decode(json_encode((array) Auth::user(), JSON_THROW_ON_ERROR), true) : null,
- ], 200, [], JSON_PRETTY_PRINT);
-})->middleware(['auth0.authorize:read:messages']);
-```
+ // Update the authenticated user with a randomly assigned favorite color.
+ Auth0::management()->users()->update(
+ id: auth()->id(),
+ body: [
+ 'user_metadata' => [
+ 'color' => $colors[random_int(0, count($colors) - 1)]
+ ]
+ ]
+ );
-The `auth0.authorize.optional` middleware will resolve an available Access Token, but won't block requests without one. This is useful when you want to treat tokenless requests as "guests":
+ // Retrieve the user's updated profile.
+ $profile = Auth0::management()->users()->get(auth()->id());
-```php
-Route::get('/api/public', function () {
- return response()->json([
- 'message' => 'Hello from a public endpoint! You don\'t need to be authenticated to see this.',
- 'authorized' => Auth::check(),
- 'user' => Auth::check() ? json_decode(json_encode((array) Auth::user(), JSON_THROW_ON_ERROR), true) : null,
- ], 200, [], JSON_PRETTY_PRINT);
-})->middleware(['auth0.authorize.optional']);
+ // Convert the PSR-7 response into a native array.
+ $profile = Auth0::json($profile);
+
+ // Extract some values from the user's profile.
+ $color = $profile['user_metadata']['color'] ?? 'unknown';
+ $name = auth()->user()->name;
+
+ return response("Hello {$name}! Your favorite color is {$color}.");
+})->middleware('auth');
```
+
+All the SDK's Management API methods are [documented here](./docs/Management.md).
+
-## Support Policy
+## Documentation
+
+- [Installation](./docs/Installation.md) — Installing the SDK and generating configuration files.
+- [Configuration](./docs/Configuration.md) — Configuring the SDK using JSON files or environment variables.
+- [Sessions](./docs/Sessions.md) — Guidance on deciding which Laravel Session API driver to use.
+- [Cookies](./docs/Cookies.md) — Important notes about using Laravel's Cookie session driver, and alternative options.
+- [Management API](./docs/Management.md) — Using the SDK to work with the [Auth0 Management API](https://auth0.com/docs/api/management/v2).
+- [Users](./docs/Users.md) — Extending the SDK to support persistent storage and [Eloquent](https://laravel.com/docs/eloquent) models.
+- [Events](./docs/Events.md) — Hooking into SDK [events](https://laravel.com/docs/events) to respond to specific actions.
+- [Deployment](./docs/Deployment.md) — Deploying your application to production.
+
+You may find the following integration guidance useful:
-Our support windows are determined by the [Laravel release support](https://laravel.com/docs/releases#support-policy) and [PHP release support](https://www.php.net/supported-versions.php) schedules, and support ends when either the Laravel framework or PHP runtime outlined below stop receiving security fixes, whichever may come first.
+- [Laravel Eloquent](./docs/Eloquent.md) — [Eloquent ORM](https://laravel.com/docs/eloquent) is supported.
+- [Laravel Octane](./docs/Octane.md) — [Octane](https://laravel.com/docs/octane) is not supported at this time.
+- [Laravel Telescope](./docs/Telescope.md) — [Telescope](https://laravel.com/docs/telescope) is compatible as of SDK v7.11.0.
-| SDK Version | Laravel Version¹ | PHP Version² | Support Ends³ |
-|-------------|------------------|--------------|---------------|
-| 7 | 9 | 8.1 | Feb 2024 |
-| | | 8.0 | Nov 2023 |
-| | 8 | 8.1 | Jan 2023 |
-| | | 8.0 | Jan 2023 |
-| 6⁴ | 8 | 8.1 | Jan 2023 |
-| | | 8.0 | Jan 2023 |
+You may also find the following resources helpful:
-Deprecations of EOL'd language or framework versions are not considered a breaking change, as Composer handles these scenarios elegantly. Legacy applications will stop receiving updates from us, but will continue to function on those unsupported SDK versions.
+- [Auth0 Documentation Hub](https://www.auth0.com/docs)
+- [Auth0 Management API Explorer](https://auth0.com/docs/api/management/v2)
+- [Auth0 Authentication API Explorer](https://auth0.com/docs/api/authentication)
-## Octane Support
+Contributions to improve our documentation [are welcomed](https://github.com/auth0/laravel-auth0/pull).
-Octane compatibility is currently considered experimental and unsupported.
+## QuickStarts
-Although we are working toward ensuring the SDK is fully compatible with this feature, we do not recommend using this with our SDK in production until we have full confidence and announced support. Due to the aggressive changes Octane makes to Laravel's core behavior, there is opportunity for problems we haven't fully identified or resolved yet.
+- [Session-based Authentication](https://auth0.com/docs/quickstart/webapp/laravel) ([GitHub](https://github.com/auth0-samples/laravel))
+- [Token-based Authorization](https://auth0.com/docs/quickstart/backend/laravel) ([GitHub](https://github.com/auth0-samples/laravel))
-Feedback and bug fix contributions are greatly appreciated as we work toward full. Octane support.
+## Community
-## Feedback
+The [Auth0 Community](https://community.auth0.com) is where you can get support, ask questions, and share your projects.
-### Contributing
+## Contributing
-We appreciate feedback and contribution to this repo! Before you get started, please see the following:
+We appreciate feedback and contributions to this library. Before you get started, please review Auth0's [General Contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md).
-- [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
-- [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
+The [Contribution Guide](./.github/CONTRIBUTING.md) contains information about our development process and expectations, insight into how to propose bug fixes and improvements, and instructions on how to build and test changes to the library.
-### Raise an issue
-To provide feedback or report a bug, [please raise an issue on our issue tracker](https://github.com/auth0/laravel-auth0/issues).
+To provide feedback or report a bug, [please raise an issue](https://github.com/auth0/laravel-auth0/issues).
-### Vulnerability Reporting
-Please do not report security vulnerabilities on the public Github issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues.
+## Code of Conduct
+
+Participants are expected to adhere to Auth0's [Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) when interacting with this project.
+
+## Security
+
+If you believe you have found a security vulnerability, we encourage you to responsibly disclose this and not open a public issue. We will investigate all reports. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues.
+
+## License
+
+This library is open-sourced software licensed under the [MIT license](./LICENSE.md).
---
@@ -258,6 +444,4 @@ Please do not report security vulnerabilities on the public Github issue tracker
-
Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout Why Auth0?
-
-
This project is licensed under the MIT license. See the LICENSE file for more info.
+
Auth0 is an easy-to-implement, adaptable authentication and authorization platform. To learn more, check out "Why Auth0?"
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..bfdf8bee
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,12 @@
+codecov:
+ range: "95...100"
+ status:
+ project:
+ patch:
+ changes:
+ignore:
+ - "src/Contract"
+ - "src/Event"
+ - "src/Exception"
+ - "src/Http"
+ - "src/Model"
diff --git a/composer.json b/composer.json
index 54da0bf0..62c0667d 100644
--- a/composer.json
+++ b/composer.json
@@ -64,28 +64,30 @@
"prefer-stable": true,
"autoload": {
"psr-4": {
- "Auth0\\Laravel\\": "src"
+ "Auth0\\Laravel\\": [
+ "src/",
+ "deprecated/"
+ ]
}
},
"autoload-dev": {
"psr-4": {
- "Auth0\\Laravel\\Tests\\": "tests"
+ "Auth0\\Laravel\\Tests\\": "tests/"
}
},
"config": {
"allow-plugins": {
+ "ergebnis/composer-normalize": true,
"pestphp/pest-plugin": true,
- "wikimedia/composer-merge-plugin": true,
- "ergebnis/composer-normalize": true
+ "php-http/discovery": false,
+ "wikimedia/composer-merge-plugin": true
},
"optimize-autoloader": true,
+ "preferred-install": "dist",
+ "process-timeout": 0,
"sort-packages": true
},
"extra": {
- "composer-normalize": {
- "indent-size": 4,
- "indent-style": "space"
- },
"laravel": {
"aliases": {
"Auth0": "Auth0\\Laravel\\Facade\\Auth0"
@@ -108,14 +110,23 @@
}
},
"scripts": {
- "phpstan": "@php vendor/bin/phpstan analyse",
- "phpstan:pro": "@php vendor/bin/phpstan analyse --pro",
- "pint": "@php vendor/bin/pint --test",
- "pint:fix": "@php vendor/bin/pint",
+ "pest": "@php vendor/bin/pest --order-by random --fail-on-risky --parallel",
+ "pest:coverage": "@php vendor/bin/pest --order-by random --fail-on-risky --coverage --parallel",
+ "pest:debug": "@php vendor/bin/pest --log-events-verbose-text pest.log --display-errors --fail-on-risky",
+ "pest:profile": "@php vendor/bin/pest --profile",
+ "phpcs": "@php vendor/bin/php-cs-fixer fix --dry-run --diff",
+ "phpcs:fix": "@php vendor/bin/php-cs-fixer fix",
+ "phpstan": "@php vendor/bin/phpstan analyze",
"psalm": "@php vendor/bin/psalm",
"psalm:fix": "@php vendor/bin/psalter --issues=all",
"rector": "@php vendor/bin/rector process src --dry-run",
"rector:fix": "@php vendor/bin/rector process src",
- "test": "@php vendor/bin/pest --order-by random"
+ "test": [
+ "@pest",
+ "@phpstan",
+ "@psalm",
+ "@rector",
+ "@phpcs"
+ ]
}
}
diff --git a/config/auth0.php b/config/auth0.php
index d9db2ffd..37959396 100644
--- a/config/auth0.php
+++ b/config/auth0.php
@@ -2,58 +2,66 @@
declare(strict_types=1);
-/**
- * Please review available configuration options here:
- * https://github.com/auth0/auth0-PHP#configuration-options.
- */
-return [
- // Should be assigned either 'api', 'management', or 'webapp' to indicate your application's use case for the SDK.
- // Determines what configuration options will be required.
- 'strategy' => env('AUTH0_STRATEGY', \Auth0\SDK\Configuration\SdkConfiguration::STRATEGY_REGULAR),
-
- // Auth0 domain for your tenant, found in your Auth0 Application settings.
- 'domain' => env('AUTH0_DOMAIN'),
-
- // If you have configured Auth0 to use a custom domain, configure it here.
- 'customDomain' => env('AUTH0_CUSTOM_DOMAIN'),
-
- // Client ID, found in the Auth0 Application settings.
- 'clientId' => env('AUTH0_CLIENT_ID'),
-
- // Authentication callback URI, as defined in your Auth0 Application settings.
- 'redirectUri' => env('AUTH0_REDIRECT_URI', env('APP_URL') . '/callback'),
-
- // Client Secret, found in the Auth0 Application settings.
- 'clientSecret' => env('AUTH0_CLIENT_SECRET'),
-
- // One or more API identifiers, found in your Auth0 API settings. The SDK uses the first value for building links. If provided, at least one of these values must match the 'aud' claim to validate an ID Token successfully.
- 'audience' => \Auth0\Laravel\Configuration::stringToArrayOrNull(env('AUTH0_AUDIENCE')),
-
- // One or more scopes to request for Tokens. See https://auth0.com/docs/scopes
- 'scope' => \Auth0\Laravel\Configuration::stringToArray(env('AUTH0_SCOPE')),
-
- // One or more Organization IDs, found in your Auth0 Organization settings. The SDK uses the first value for building links. If provided, at least one of these values must match the 'org_id' claim to validate an ID Token successfully.
- 'organization' => \Auth0\Laravel\Configuration::stringToArrayOrNull(env('AUTH0_ORGANIZATION')),
-
- // The secret used to derive an encryption key for the user identity in a session cookie and to sign the transient cookies used by the login callback.
- 'cookieSecret' => env('AUTH0_COOKIE_SECRET', env('APP_KEY')),
-
- // How long, in seconds, before cookies expire. If set to 0 the cookie will expire at the end of the session (when the browser closes).
- 'cookieExpires' => (int) env('AUTH0_COOKIE_EXPIRES', 0),
-
- // Cookie domain, for example 'www.example.com', for use with PHP sessions and SDK cookies. Defaults to value of HTTP_HOST server environment information.
- // Note: To make cookies visible on all subdomains then the domain must be prefixed with a dot like '.example.com'.
- 'cookieDomain' => env('AUTH0_COOKIE_DOMAIN'),
-
- // Specifies path on the domain where the cookies will work. Defaults to '/'. Use a single slash ('/') for all paths on the domain.
- 'cookiePath' => env('AUTH0_COOKIE_PATH', '/'),
-
- // Defaults to false. Specifies whether cookies should ONLY be sent over secure connections.
- 'cookieSecure' => \Auth0\Laravel\Configuration::stringToBoolOrNull(env('AUTH0_COOKIE_SECURE'), false),
+use Auth0\Laravel\Configuration;
+use Auth0\SDK\Configuration\SdkConfiguration;
+
+return Configuration::VERSION_2 + [
+ 'registerGuards' => true,
+ 'registerMiddleware' => true,
+ 'registerAuthenticationRoutes' => true,
+ 'configurationPath' => null,
+
+ 'guards' => [
+ 'default' => [
+ Configuration::CONFIG_STRATEGY => Configuration::get(Configuration::CONFIG_STRATEGY, SdkConfiguration::STRATEGY_NONE),
+ Configuration::CONFIG_DOMAIN => Configuration::get(Configuration::CONFIG_DOMAIN),
+ Configuration::CONFIG_CUSTOM_DOMAIN => Configuration::get(Configuration::CONFIG_CUSTOM_DOMAIN),
+ Configuration::CONFIG_CLIENT_ID => Configuration::get(Configuration::CONFIG_CLIENT_ID),
+ Configuration::CONFIG_CLIENT_SECRET => Configuration::get(Configuration::CONFIG_CLIENT_SECRET),
+ Configuration::CONFIG_AUDIENCE => Configuration::get(Configuration::CONFIG_AUDIENCE),
+ Configuration::CONFIG_ORGANIZATION => Configuration::get(Configuration::CONFIG_ORGANIZATION),
+ Configuration::CONFIG_USE_PKCE => Configuration::get(Configuration::CONFIG_USE_PKCE),
+ Configuration::CONFIG_SCOPE => Configuration::get(Configuration::CONFIG_SCOPE),
+ Configuration::CONFIG_RESPONSE_MODE => Configuration::get(Configuration::CONFIG_RESPONSE_MODE),
+ Configuration::CONFIG_RESPONSE_TYPE => Configuration::get(Configuration::CONFIG_RESPONSE_TYPE),
+ Configuration::CONFIG_TOKEN_ALGORITHM => Configuration::get(Configuration::CONFIG_TOKEN_ALGORITHM),
+ Configuration::CONFIG_TOKEN_JWKS_URI => Configuration::get(Configuration::CONFIG_TOKEN_JWKS_URI),
+ Configuration::CONFIG_TOKEN_MAX_AGE => Configuration::get(Configuration::CONFIG_TOKEN_MAX_AGE),
+ Configuration::CONFIG_TOKEN_LEEWAY => Configuration::get(Configuration::CONFIG_TOKEN_LEEWAY),
+ Configuration::CONFIG_TOKEN_CACHE => Configuration::get(Configuration::CONFIG_TOKEN_CACHE),
+ Configuration::CONFIG_TOKEN_CACHE_TTL => Configuration::get(Configuration::CONFIG_TOKEN_CACHE_TTL),
+ Configuration::CONFIG_HTTP_MAX_RETRIES => Configuration::get(Configuration::CONFIG_HTTP_MAX_RETRIES),
+ Configuration::CONFIG_HTTP_TELEMETRY => Configuration::get(Configuration::CONFIG_HTTP_TELEMETRY),
+ Configuration::CONFIG_MANAGEMENT_TOKEN => Configuration::get(Configuration::CONFIG_MANAGEMENT_TOKEN),
+ Configuration::CONFIG_MANAGEMENT_TOKEN_CACHE => Configuration::get(Configuration::CONFIG_MANAGEMENT_TOKEN_CACHE),
+ Configuration::CONFIG_CLIENT_ASSERTION_SIGNING_KEY => Configuration::get(Configuration::CONFIG_CLIENT_ASSERTION_SIGNING_KEY),
+ Configuration::CONFIG_CLIENT_ASSERTION_SIGNING_ALGORITHM => Configuration::get(Configuration::CONFIG_CLIENT_ASSERTION_SIGNING_ALGORITHM),
+ Configuration::CONFIG_PUSHED_AUTHORIZATION_REQUEST => Configuration::get(Configuration::CONFIG_PUSHED_AUTHORIZATION_REQUEST),
+ Configuration::CONFIG_BACKCHANNEL_LOGOUT_CACHE => Configuration::get(Configuration::CONFIG_BACKCHANNEL_LOGOUT_CACHE),
+ Configuration::CONFIG_BACKCHANNEL_LOGOUT_EXPIRES => Configuration::get(Configuration::CONFIG_BACKCHANNEL_LOGOUT_EXPIRES),
+ ],
+
+ 'api' => [
+ Configuration::CONFIG_STRATEGY => SdkConfiguration::STRATEGY_API,
+ ],
+
+ 'web' => [
+ Configuration::CONFIG_STRATEGY => SdkConfiguration::STRATEGY_REGULAR,
+ Configuration::CONFIG_COOKIE_SECRET => Configuration::get(Configuration::CONFIG_COOKIE_SECRET, env('APP_KEY')),
+ Configuration::CONFIG_REDIRECT_URI => Configuration::get(Configuration::CONFIG_REDIRECT_URI, env('APP_URL') . '/callback'),
+ Configuration::CONFIG_SESSION_STORAGE => Configuration::get(Configuration::CONFIG_SESSION_STORAGE),
+ Configuration::CONFIG_SESSION_STORAGE_ID => Configuration::get(Configuration::CONFIG_SESSION_STORAGE_ID),
+ Configuration::CONFIG_TRANSIENT_STORAGE => Configuration::get(Configuration::CONFIG_TRANSIENT_STORAGE),
+ Configuration::CONFIG_TRANSIENT_STORAGE_ID => Configuration::get(Configuration::CONFIG_TRANSIENT_STORAGE_ID),
+ ],
+ ],
- // Named routes within your Laravel application that the SDK may call during stateful requests for redirections.
'routes' => [
- 'home' => env('AUTH0_ROUTE_HOME', '/'),
- 'login' => env('AUTH0_ROUTE_LOGIN', 'login'),
+ Configuration::CONFIG_ROUTE_INDEX => Configuration::get(Configuration::CONFIG_ROUTE_INDEX, '/'),
+ Configuration::CONFIG_ROUTE_CALLBACK => Configuration::get(Configuration::CONFIG_ROUTE_CALLBACK, '/callback'),
+ Configuration::CONFIG_ROUTE_LOGIN => Configuration::get(Configuration::CONFIG_ROUTE_LOGIN, '/login'),
+ Configuration::CONFIG_ROUTE_AFTER_LOGIN => Configuration::get(Configuration::CONFIG_ROUTE_AFTER_LOGIN, '/'),
+ Configuration::CONFIG_ROUTE_LOGOUT => Configuration::get(Configuration::CONFIG_ROUTE_LOGOUT, '/logout'),
+ Configuration::CONFIG_ROUTE_AFTER_LOGOUT => Configuration::get(Configuration::CONFIG_ROUTE_AFTER_LOGOUT, '/'),
],
];
diff --git a/deprecated/Cache/LaravelCacheItem.php b/deprecated/Cache/LaravelCacheItem.php
new file mode 100644
index 00000000..391641d8
--- /dev/null
+++ b/deprecated/Cache/LaravelCacheItem.php
@@ -0,0 +1,56 @@
+expiration = match (true) {
+ null === $time => new DateTimeImmutable('now +1 year'),
+ is_int($time) => new DateTimeImmutable('now +' . (string) $time . ' seconds'),
+ $time instanceof DateInterval => (new DateTimeImmutable())->add($time),
+ };
+
+ return $this;
+ }
+
+ public function expiresAt(?DateTimeInterface $expiration): static
+ {
+ $this->expiration = $expiration ?? new DateTimeImmutable('now +1 year');
+
+ return $this;
+ }
+
+ public function set(mixed $value): static
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+
+ public static function miss(string $key): self
+ {
+ return new self(
+ key: $key,
+ value: null,
+ hit: false,
+ );
+ }
+}
diff --git a/deprecated/Cache/LaravelCachePool.php b/deprecated/Cache/LaravelCachePool.php
new file mode 100644
index 00000000..0641f7d5
--- /dev/null
+++ b/deprecated/Cache/LaravelCachePool.php
@@ -0,0 +1,18 @@
+has('logout_token')) {
+ app('auth0')->handleBackchannelLogout($request->string('logout_token', '')->trim());
+ }
+});
+```
+
+2. **Configure your Auth0 tenant to use Backchannel Logout.** See the [Auth0 documentation](https://auth0.com/docs/authenticate/login/logout/back-channel-logout/configure-back-channel-logout) for more information on how to do this. Please ensure you point the Logout URI to the backchannel route we just added to your application.
+
+Note: If your application's configuration assigns `false` to the `backchannelLogoutCache` SDK configuration property, this feature will be disabled entirely.
diff --git a/docs/Configuration.md b/docs/Configuration.md
new file mode 100644
index 00000000..8f9b5876
--- /dev/null
+++ b/docs/Configuration.md
@@ -0,0 +1,385 @@
+# Configuration
+
+- [SDK Configuration](#sdk-configuration)
+ - [JSON Configuration Files](#json-configuration-files)
+ - [Environment Variables](#environment-variables)
+ - [Order of Priority](#order-of-priority)
+ - [Default Behavior](#default-behavior)
+ - [Guard Registration](#guard-registration)
+ - [Middleware Registration](#middleware-registration)
+ - [Route Registration](#route-registration)
+- [Auth0 Configuration](#auth0-configuration)
+ - [Auth0 Applications](#auth0-applications)
+ - [Creating Applications with the CLI](#creating-applications-with-the-cli)
+ - [Creating Applications Manually](#creating-applications-manually)
+ - [Modifying Applications with the CLI](#modifying-applications-using-the-cli)
+ - [Modifying Applications Manually](#modifying-applications-manually)
+ - [Auth0 APIs](#auth0-apis)
+ - [Creating APIs with the CLI](#creating-apis-with-the-cli)
+ - [Creating APIs Manually](#creating-apis-manually)
+ - [Modifying APIs with the CLI](#modifying-apis-using-the-cli)
+ - [Modifying APIs Manually](#modifying-apis-manually)
+
+## SDK Configuration
+
+This guide addresses v2 of the SDK configuration format. You can determine which version you are using by evaluating the constant prepended to the returned array in your application's `config/auth0.php` file, prefixed with `Configuration::VERSION_`. For example:
+
+```php
+return Configuration::VERSION_2 + [
+ // ...
+];
+```
+
+If you do not see such a value, you are most likely using an outdated configuration format, and should upgrade by running `php artisan vendor:publish --tag auth0 --force` from your project directory. You will lose any alterations you have made to this file in the process.
+
+### JSON Configuration Files
+
+The preferred method of SDK configuration is to use JSON exported from the [Auth0 CLI](https://auth0.com/docs/cli). This allows you to use the CLI to manage your Auth0 configuration, and then export the configuration to JSON for use by the SDK.
+
+The SDK will look for the following files in the project directory, in the order listed:
+
+- `auth0.json`
+- `auth0..json`
+- `auth0.api.json`
+- `auth0.app.json`
+- `auth0.api..json`
+- `auth0.app..json`
+
+Where `` is the value of Laravel's `APP_ENV` environment variable (if set.) Duplicate keys in the files listed above will be overwritten in the order listed.
+
+### Environment Variables
+
+The SDK also supports configuration using environment variables. These can be defined within the host environment, or using so-called dotenv (`.env`, or `.env.*`) files in the project directory.
+
+| Variable | Description |
+| --------------------- | ---------------------------------------------------------------------------------------------------- |
+| `AUTH0_DOMAIN` | `String (FQDN)` The Auth0 domain for your tenant. |
+| `AUTH0_CUSTOM_DOMAIN` | `String (FQDN)` The Auth0 custom domain for your tenant, if set. |
+| `AUTH0_CLIENT_ID` | `String` The Client ID for your Auth0 application. |
+| `AUTH0_CLIENT_SECRET` | `String` The Client Secret for your Auth0 application. |
+| `AUTH0_AUDIENCE` | `String (comma-delimited list)` The audiences for your application. |
+| `AUTH0_SCOPE` | `String (comma-delimited list)` The scopes for your application. Defaults to 'openid,profile,email'. |
+| `AUTH0_ORGANIZATION` | `String (comma-delimited list)` The organizations for your application. |
+
+The following environment variables are supported, but should not be adjusted unless you know what you are doing:
+
+| Variable | Description |
+| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `AUTH0_USE_PKCE` | Boolean. Whether to use PKCE for the authorization flow. Defaults to `true`. |
+| `AUTH0_RESPONSE_MODE` | `String` The response mode to use for the authorization flow. Defaults to `query`. |
+| `AUTH0_RESPONSE_TYPE` | `String` The response type to use for the authorization flow. Defaults to `code`. |
+| `AUTH0_TOKEN_ALGORITHM` | `String` The algorithm to use for the ID token. Defaults to `RS256`. |
+| `AUTH0_TOKEN_JWKS_URI` | `String (URL)` The URI to use to retrieve the JWKS for the ID token. Defaults to `https:///.well-known/jwks.json`. |
+| `AUTH0_TOKEN_MAX_AGE` | `Integer` The maximum age of a token, in seconds. No default value is assigned. |
+| `AUTH0_TOKEN_LEEWAY` | `Integer` The leeway to use when validating a token, in seconds. Defaults to `60` (1 minute). |
+| `AUTH0_TOKEN_CACHE` | `String (class name)` A PSR-6 class to use for caching JWKS responses. |
+| `AUTH0_TOKEN_CACHE_TTL` | `Integer` The TTL to use for caching JWKS responses. Defaults to `60` (1 minute). |
+| `AUTH0_HTTP_MAX_RETRIES` | `Integer` The maximum number of times to retry a failed HTTP request. Defaults to `3`. |
+| `AUTH0_HTTP_TELEMETRY` | `Boolean` Whether to send telemetry data with HTTP requests to Auth0. Defaults to `true`. |
+| `AUTH0_SESSION_STORAGE` | `String (class name)` The `StoreInterface` class to use for storing session data. Defaults to using Laravel's native Sessions API. |
+| `AUTH0_SESSION_STORAGE_ID` | `String` The namespace to use for storing session data. Defaults to `auth0_session`. |
+| `AUTH0_TRANSIENT_STORAGE` | `String (class name)` The `StoreInterface` class to use for storing temporary session data. Defaults to using Laravel's native Sessions API. |
+| `AUTH0_TRANSIENT_STORAGE_ID` | `String` The namespace to use for storing temporary session data. Defaults to `auth0_transient`. |
+| `AUTH0_MANAGEMENT_TOKEN` | `String` The Management API token to use for the Management API client. If one is not provided, the SDK will attempt to create one for you. |
+| `AUTH0_MANAGEMENT_TOKEN_CACHE` | `Integer` A PSR-6 class to use for caching Management API tokens. |
+| `AUTH0_CLIENT_ASSERTION_SIGNING_KEY` | `String` The key to use for signing client assertions. |
+| `AUTH0_CLIENT_ASSERTION_SIGNING_ALGORITHM` | `String` The algorithm to use for signing client assertions. Defaults to `RS256`. |
+| `AUTH0_PUSHED_AUTHORIZATION_REQUEST` | `Boolean` Whether the SDK should use Pushed Authorization Requests during authentication. Note that your tenant must have this feature enabled. Defaults to `false`. |
+| `AUTH0_BACKCHANNEL_LOGOUT_CACHE` | `String (class name)` A PSR-6 class to use for caching backchannel logout tokens. |
+| `AUTH0_BACKCHANNEL_LOGOUT_EXPIRES` | `Integer` How long (in seconds) to cache a backchannel logout token. Defaults to `2592000` (30 days). |
+
+### Order of Priority
+
+The SDK collects configuration data from multiple potential sources, in the following order:
+
+- `.auth0.json` files
+- `.env` (dotenv) files
+- Host environment variables
+
+> **Note:**
+> In the filenames listed below, `%APP_ENV%` is replaced by the application's configured `APP_ENV` environment variable, if one is set.
+
+It begins by loading matching JSON configuration files from the project's root directory, in the following order:
+
+- `.auth0.json`
+- `.auth0.%APP_ENV%.json`
+- `.auth0.api.json`
+- `.auth0.app.json`
+- `.auth0.api.%APP_ENV%.json`
+- `.auth0.app.%APP_ENV%.json`
+
+It then loads configuration data from available `.env` (dotenv) configuration files, in the following order.
+
+- `.env`
+- `.env.auth0`
+- `.env.%APP_ENV%`
+- `.env.%APP_ENV%.auth0`
+
+Finally, it loads environment variables from the host environment.
+
+Duplicate configuration data is overwritten by the value from the last source loaded. For example, if the `AUTH0_DOMAIN` environment variable is set in both the `.env` file and the host environment, the value from the host environment will be used.
+
+Although JSON configuration keys are different from their associated environment variable counterparts, these are translated automatically by the SDK. For example, the `domain` key in the JSON configuration files is translated to the `AUTH0_DOMAIN` environment variable.
+
+### Default Behavior
+
+#### Guard Registration
+
+By default, the SDK will register the Authentication and Authorization guards with your Laravel application, as well as a compatible [User Provider](./Users.md).
+
+You can disable this behavior by setting `registerGuards` to `false` in your `config/auth0.php` file.
+
+```php
+return Configuration::VERSION_2 + [
+ 'registerGuards' => false,
+ // ...
+];
+```
+
+To register the guards manually, update the arrays in your `config/auth.php` file to include the following additions:
+
+```php
+'guards' => [
+ 'auth0-session' => [
+ 'driver' => 'auth0.authenticator',
+ 'provider' => 'auth0-provider',
+ 'configuration' => 'web',
+ ],
+ 'auth0-api' => [
+ 'driver' => 'auth0.authorizer',
+ 'provider' => 'auth0-provider',
+ 'configuration' => 'api',
+ ],
+],
+
+'providers' => [
+ 'auth0-provider' => [
+ 'driver' => 'auth0.provider',
+ 'repository' => 'auth0.repository',
+ ],
+],
+```
+
+#### Middleware Registration
+
+By default, the SDK will register the Authentication and Authorization guards within your application's `web` and `api` middleware groups.
+
+You can disable this behavior by setting `registerMiddleware` to `false` in your `config/auth0.php` file.
+
+```php
+return Configuration::VERSION_2 + [
+ 'registerMiddleware' => false,
+ // ...
+];
+```
+
+To register the middleware manually, update your `app/Http/Kernel.php` file and include the following additions:
+
+```php
+protected $middlewareGroups = [
+ 'web' => [
+ \Auth0\Laravel\Middleware\AuthenticatorMiddleware::class,
+ // ...
+ ],
+
+ 'api' => [
+ \Auth0\Laravel\Middleware\AuthorizerMiddleware::class,
+ // ...
+ ],
+];
+```
+
+Alternatively, you can assign the guards to specific routes by using the `Auth` facade. For `routes/web.php`, add the following before any routes:
+
+```php
+Auth::shouldUse('auth0-session');
+```
+
+For `routes/api.php`, add the following before any routes:
+
+```php
+Auth::shouldUse('auth0-api');
+```
+
+#### Route Registration
+
+By default, the SDK will register the following routes for authentication:
+
+| Method | URI | Name | Controller | Purpose |
+| ------ | ----------- | ---------- | ---------------------------------------------- | ---------------------------------- |
+| `GET` | `/login` | `login` | `Auth0\Laravel\Controllers\LoginController` | Initiates the authentication flow. |
+| `GET` | `/logout` | `logout` | `Auth0\Laravel\Controllers\LogoutController` | Logs the user out. |
+| `GET` | `/callback` | `callback` | `Auth0\Laravel\Controllers\CallbackController` | Handles the callback from Auth0. |
+
+You can disable this behavior by setting `registerAuthenticationRoutes` to `false` in your `config/auth0.php` file.
+
+```php
+return Configuration::VERSION_2 + [
+ 'registerAuthenticationRoutes' => false,
+ // ...
+];
+```
+
+If you've disabled the automatic registration of routes, you must register the routes manually for authentication to work.
+
+```php
+use Auth0\Laravel\Controllers\{LoginController, LogoutController, CallbackController};
+
+Route::group(['middleware' => ['guard:auth0-session']], static function (): void {
+ Route::get('/login', LoginController::class)->name('login');
+ Route::get('/logout', LogoutController::class)->name('logout');
+ Route::get('/callback', CallbackController::class)->name('callback');
+});
+```
+
+Or you can call the SDK Facade's `routes()` method in your `routes/web.php` file:
+
+```php
+Auth0::routes();
+```
+
+- These must be registered within the `web` middleware group, as they rely on sessions.
+- Requests must be routed through the SDK's Authenticator guard.
+
+## Auth0 Configuration
+
+The following guidance is provided to help you configure your Auth0 tenant for use with the SDK. It is not intended to be a comprehensive guide to configuring Auth0. Please refer to the [Auth0 documentation](https://auth0.com/docs) for more information.
+
+### Auth0 Applications
+
+#### Creating Applications with the CLI
+
+Use the CLI's `apps create` command to create a new Auth0 Application:
+
+```shell
+auth0 apps create \
+ --name "My Laravel Application" \
+ --type "regular" \
+ --auth-method "post" \
+ --callbacks "http://localhost:8000/callback" \
+ --logout-urls "http://localhost:8000" \
+ --reveal-secrets \
+ --no-input
+```
+
+If you are configuring the SDK for this application, make note of the `client_id` and `client_secret` values returned by the command. Follow the guidance in the [configuration guide](#configuration) to configure the SDK using these values.
+
+The following parameters used in this example are of note:
+
+- `--type` - The [application type](https://auth0.com/docs/get-started/applications).
+ - For Laravel applications, this should always be set to `regular`.
+- `--auth-method` - This represents the 'Token Endpoint Authentication Method' used for authentication.
+ - For Laravel applications, this should always be set to `post`.
+- `--callbacks` - The callback URLs to use for authentication.
+ - In development, this should be set to `http://localhost:8000/callback` or as appropriate.
+ - In production, adjust this value to match your application's Internet-accessible URL for its`/callback`` route.
+ - This value can be a comma-separated list of URLs.
+- `--logout-urls` - The logout URLs to use for authentication.
+ - In development, this should be set to `http://localhost:8000` or as appropriate.
+ - In production, adjust this value to match where your application redirects end users after logging out. The value should be an Internet-accessible URL.
+ - This value can be a comma-separated list of URLs.
+
+Please refer to the [CLI documentation](https://auth0.github.io/auth0-cli/auth0_apps_create.html) for additional information on the `apps create` command.
+
+#### Modifying Applications using the CLI
+
+Use the CLI's `apps update` command to create a new Auth0 API:
+
+```shell
+auth0 apps update %CLIENT_ID% \
+ --name "My Updated Laravel Application" \
+ --callbacks "https://production/callback,http://localhost:8000/callback" \
+ --logout-urls "https://production/logout,http://localhost:8000" \
+ --no-input
+```
+
+Substitute `%CLIENT_ID%` with your application's Client ID. Depending on how you configured the SDK, this value can be found:
+
+- In the `.auth0.app.json` file in your project's root directory, as the `client_id` property value.
+- In the `.env` file in your project's root directory, as the `AUTH0_CLIENT_ID` property value.
+- As the `AUTH0_CLIENT_ID` environment variable.
+- Evaluating the output from the CLI's `apps list` command.
+
+Please refer to the [CLI documentation](https://auth0.github.io/auth0-cli/auth0_apps_update.html) for additional information on the `apps update` command.
+
+#### Creating Applications Manually
+
+1. Log in to your [Auth0 Dashboard](https://manage.auth0.com/).
+2. Click the **Applications** menu item in the left navigation bar.
+3. Click the **Create Application** button.
+4. Enter a name for your application.
+5. Select **Regular Web Applications** as the application type.
+6. Click the **Create** button.
+7. Click the **Settings** tab.
+8. Set the **Token Endpoint Authentication Method** to `POST`.
+9. Set the **Allowed Callback URLs** to `http://localhost:8000/callback` or as appropriate.
+10. Set the **Allowed Logout URLs** to `http://localhost:8000` or as appropriate.
+11. Click the **Save Changes** button.
+
+#### Modifying Applications Manually
+
+1. Log in to your [Auth0 Dashboard](https://manage.auth0.com/).
+2. Click the **Applications** menu item in the left navigation bar.
+3. Click the name of the application you wish to modify.
+4. Click the **Settings** tab.
+5. Modify the properties you wish to update as appropriate.
+6. Click the **Save Changes** button.
+
+### Auth0 APIs
+
+#### Creating APIs with the CLI
+
+Use the CLI's `apis create` command to create a new Auth0 API:
+
+```shell
+auth0 apis create \
+ --name "My Laravel Application API" \
+ --identifier "https://github.com/auth0/laravel-auth0" \
+ --offline-access \
+ --no-input
+```
+
+If you are configuring the SDK for this API, make note of the `identifier` you used here. Follow the guidance in the [configuration guide](#configuration) to configure the SDK using this value.
+
+The following parameters are of note:
+
+- `--identifier` - The [unique identifier](https://auth0.com/docs/get-started/apis/api-settings#general-settings) for your API, sometimes referred to as the `audience`. This can be any value you wish, but it must be unique within your account. It cannot be changed later.
+- `--offline-access` - This enables the use of [Refresh Tokens](https://auth0.com/docs/tokens/refresh-tokens) for your API. This is not required for the SDK to function.
+
+Please refer to the [CLI documentation](https://auth0.github.io/auth0-cli/auth0_apis_create.html) for additional information on the `apis create` command.
+
+#### Modifying APIs using the CLI
+
+Use the CLI's `apis update` command to create a new Auth0 API:
+
+```shell
+auth0 apis update %IDENTIFIER% \
+ --name "My Updated Laravel Application API" \
+ --token-lifetime 6100 \
+ --offline-access=false \
+ --scopes "letter:write,letter:read" \
+ --no-input
+```
+
+Substitute `%IDENTIFIER%` with your API's unique identifier.
+
+Please refer to the [CLI documentation](https://auth0.github.io/auth0-cli/auth0_apis_update.html) for additional information on the `apis update` command.
+
+#### Creating APIs Manually
+
+1. Log in to your [Auth0 Dashboard](https://manage.auth0.com/).
+2. Click the **APIs** menu item in the left navigation bar.
+3. Click the **Create API** button.
+4. Enter a name for your API.
+5. Enter a unique identifier for your API. This can be any value you wish, but it must be unique within your account. It cannot be changed later.
+6. Click the **Create** button.
+
+#### Modifying APIs Manually
+
+1. Log in to your [Auth0 Dashboard](https://manage.auth0.com/).
+2. Click the **APIs** menu item in the left navigation bar.
+3. Click the name of the API you wish to modify.
+4. Modify the properties you wish to update as appropriate.
+5. Click the **Save Changes** button.
+
+Additional information on Auth0 application settings [can be found here](https://auth0.com/docs/get-started/applications/application-settings).
diff --git a/docs/Cookies.md b/docs/Cookies.md
new file mode 100644
index 00000000..eeee3898
--- /dev/null
+++ b/docs/Cookies.md
@@ -0,0 +1,47 @@
+# Cookies
+
+We strongly recommend using the `database` or `redis` session drivers, but realize this is not always a viable option for all developers or use cases. The Auth0 Laravel SDK supports cookies for storing authentication state, but there are notable drawbacks to be aware of.
+
+## Laravel's Cookie Session Driver
+
+As noted in our [sessions documentation](./Sessions.md), Laravel's `cookie` session driver is not a reliable option for production applications as it suffers from a number of notable drawbacks:
+
+- Browsers impose a size limit of 4 KB on individual cookies, which can quickly be exceeded by storing session data.
+- Laravel's cookie driver unfortunately does not "chunk" (split up) larger cookies into multiple cookies, so it is impossible to store more than the noted 4 KB of total session data.
+- Most web servers and load balancers require additional configuration to accept and deliver larger cookie headers.
+
+## Auth0 PHP SDK's Custom Cookie Session Handler
+
+The underlying [Auth0 PHP SDK](https://github.com/auth0/auth0-PHP) (which the Auth0 Laravel SDK is built upon) includes a powerful custom cookie session handler that supports chunking of larger cookies. This approach will enable you to securely and reliably store larger authentication states for your users.
+
+It is important to note that this approach is incompatible with [Octane](./Octane.md) due to the way it delivers cookie headers.
+
+To enable this feature, assign a `cookie` string value to the `AUTH0_SESSION_STORAGE` and `AUTH0_TRANSIENT_STORAGE` environment variables (or your `.env` file.)
+
+```ini
+# Persistent session data:
+AUTH0_SESSION_STORAGE=cookie
+
+# Temporary session data (used only during authentication):
+AUTH0_TRANSIENT_STORAGE=cookie
+```
+
+This will override the SDK's default behavior of using the Laravel Sessions API, and instead use the integrated Auth0 PHP SDK's custom cookie session handler. Please note:
+
+- When this feature is enabled, all properties of cookie storage (like `sameSite`, `secure`, and so forth) must be configured independently. This approach does not use Laravel's settings. Please refer to the [Auth0 PHP SDK's documentation](https://github.com/auth0/auth0-PHP) for guidance on how to configure these.
+- By default your Laravel application's `APP_KEY` will be used to encrypt the cookie data. You can change this by assigning the `AUTH0_COOKIE_SECRET` environment variable (or your `.env` file) a string. If you do this, please ensure you are using an adequately long secure secret.
+- Please ensure your server is configured to deliver and accept cookies prefixed with `auth0_session_` and `auth0_transient_` followed by a series of numbers (beginning with 0). These are the divided content body of the authenticated session data.
+
+### Increasing Server Cookies Header Sizes
+
+You may need to configure your web server or load balancer to accept and deliver larger cookie headers. For example, if you are using Nginx you will need to set the `large_client_header_buffers` directive to a value greater than the default of 4 KB.
+
+```nginx
+large_client_header_buffers 4 16k;
+```
+
+Please refer to your web server or load balancer's documentation for more information.
+
+### Reminder on Octane Compatibility
+
+As noted above, the Auth0 PHP SDK's custom cookie session handler is incompatible with [Octane](./Octane.md) due to the way it delivers cookie headers. If you are using Octane, you must use the Laravel Sessions API with a `database` or `redis` driver.
diff --git a/docs/Deployment.md b/docs/Deployment.md
new file mode 100644
index 00000000..66ebdaee
--- /dev/null
+++ b/docs/Deployment.md
@@ -0,0 +1,196 @@
+# Deployment
+
+When you're preparing to deploy your application to production, there are some basic steps you can take to make sure your application is running as smoothly and securely as possible. In this guide, we'll cover some starting points for making sure your application is deployed properly.
+
+- [Auth0 Configuration](#auth0-configuration)
+- [TLS / HTTPS](#tls--https)
+- [Cookies](#cookies)
+- [Server Configuration](#server-configuration)
+ - [Caddy](#caddy)
+ - [Nginx](#nginx)
+ - [Apache](#apache)
+- [Optimization](#optimization)
+ - [Autoloader](#autoloader)
+ - [Dependencies](#dependencies)
+ - [Caching Configuration](#caching-configuration)
+ - [Caching Events](#caching-events)
+ - [Caching Routes](#caching-routes)
+ - [Caching Views](#caching-views)
+ - [Debug Mode](#debug-mode)
+
+## Auth0 Configuration
+
+When migrating your Laravel application from local development to production, you will need to update your Auth0 application's configuration to reflect the new URLs for your application. You can do this by logging into the [Auth0 Dashboard](https://manage.auth0.com/) and updating the following fields:
+
+- **Allowed Callback URLs**: The URL that Auth0 will redirect to after the user authenticates. This should be set to the Internet-accessible URL of your application's `/callback` route.
+- **Allowed Logout URLs**: The URL that Auth0 will redirect to after the user logs out. This should be set to an appropriate Internet-accessible URL of your application.
+
+Note that you can include multiple URLs in these fields by separating them with commas, for example `https://example.com/callback,http://localhost:8000/callback`.
+
+See [the configuration guide](/docs/configuration.md) for additional guidance on updating configuration properties.
+
+## TLS / HTTPS
+
+Auth0 requires that all applications use TLS/HTTPS. This is a requirement for all applications, regardless of whether they are running in production or development, with the exception of applications running on `localhost`. If you are running your application in a development environment, you can use a self-signed certificate. However, you should ensure that your application is running over TLS/HTTPS in production.
+
+Let's Encrypt is a great option for obtaining free TLS/HTTPS certificates for your application. You can find instructions for obtaining a certificate for your server at [https://letsencrypt.org/getting-started/](https://letsencrypt.org/getting-started/).
+
+## Cookies
+
+Depending on the integration approach, you may encounter instances where the cookies delivered by the application exceed the default allowances of your web server. This can result in errors such as `400 Bad Request`. If you encounter this issue, you should increase the header size limits of your web server to accommodate the larger cookies. The server configurations below include examples of how to do this for common web servers.
+
+You should also ensure your application's `config/session.php` file is configured securely. The default configuration provided by Laravel is a great starting point, but you should double check that the `secure` option is set to `true`, that the `same_site` option is set to `lax` or `strict`, and the `http_only` option is set to `true`.
+
+## Server Configuration
+
+Please ensure, like all the example configurations provided below, that your web server directs all requests to your application's `public/index.php` file. You should **never** attempt to move the `index.php` file to your project's root, as serving the application from the project root will expose many sensitive configuration files to the public Internet.
+
+### Caddy
+
+```nginx
+example.com {
+ root * /var/www/example.com/public
+
+ encode zstd gzip
+ file_server
+
+ limits {
+ header 4kb
+ }
+
+ header {
+ X-XSS-Protection "1; mode=block"
+ X-Content-Type-Options "nosniff"
+ X-Frame-Options "SAMEORIGIN"
+ }
+
+ php_fastcgi unix//var/run/php/php8.2-fpm.sock
+}
+```
+
+### Nginx
+
+```nginx
+server {
+ listen 80;
+ listen [::]:80;
+ server_name example.com;
+ root /var/www/example.com/public;
+
+ add_header X-XSS-Protection "1; mode=block";
+ add_header X-Content-Type-Options "nosniff";
+ add_header X-Frame-Options "SAMEORIGIN";
+
+ large_client_header_buffers 4 32k;
+
+ index index.php;
+
+ charset utf-8;
+
+ location / {
+ try_files $uri $uri/ /index.php?$query_string;
+ }
+
+ location = /favicon.ico { access_log off; log_not_found off; }
+ location = /robots.txt { access_log off; log_not_found off; }
+
+ error_page 404 /index.php;
+
+ location ~ \.php$ {
+ fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
+ fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
+ include fastcgi_params;
+ }
+
+ location ~ /\.(?!well-known).* {
+ deny all;
+ }
+}
+```
+
+### Apache
+
+```apache
+
+ ServerName example.com
+ ServerAdmin admin@example.com
+ DocumentRoot /var/www/html/example.com/public
+
+ LimitRequestFieldSize 16384
+
+
+ AllowOverride All
+
+
+
+ Header set X-XSS-Protection "1; mode=block"
+ Header always set X-Content-Type-Options nosniff
+ Header always set X-Frame-Options SAMEORIGIN
+
+
+ ErrorLog ${APACHE_LOG_DIR}/error.log
+ CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+```
+
+## Optimization
+
+### Autoloader
+
+When deploying to production, make sure that you are optimizing Composer's class autoloader map so Composer can quickly find the proper file to load for a given class:
+
+```shell
+composer install --optimize-autoloader --no-dev
+```
+
+Be sure to use the `--no-dev` option in production. This will prevent Composer from installing any development dependencies your project's dependencies may have.
+
+### Dependencies
+
+You should include your `composer.lock` file in your project's source control repository. Fo project's dependencies can be installed much faster with this file is present. Your production environment does not run `composer update` directly. Instead, you can run the `composer update` command locally when you want to update your dependencies, and then commit the updated `composer.lock` file to your repository. Be sure you are running the same major PHP version as your production environment, to avoid introducing compatibility issues.
+
+Because the `composer.lock` file includes specific versions of your dependencies, other developers on your team will be using the same versions of the dependencies as you. This will help prevent bugs and compatibility issues from appearing in production that aren't present during development.
+
+### Caching Configuration
+
+When deploying your application to production, you should make sure that you run the config:cache Artisan command during your deployment process:
+
+```shell
+php artisan config:cache
+```
+
+This command will combine all of Laravel's configuration files into a single, cached file, which greatly reduces the number of trips the framework must make to the filesystem when loading your configuration values.
+
+### Caching Events
+
+If your application is utilizing event discovery, you should cache your application's event to listener mappings during your deployment process. This can be accomplished by invoking the event:cache Artisan command during deployment:
+
+```shell
+php artisan event:cache
+```
+
+### Caching Routes
+
+If you are building a large application with many routes, you should make sure that you are running the route:cache Artisan command during your deployment process:
+
+```shell
+php artisan route:cache
+```
+
+This command reduces all of your route registrations into a single method call within a cached file, improving the performance of route registration when registering hundreds of routes.
+
+### Caching Views
+
+When deploying your application to production, you should make sure that you run the view:cache Artisan command during your deployment process:
+
+```shell
+php artisan view:cache
+```
+
+This command precompiles all your Blade views so they are not compiled on demand, improving the performance of each request that returns a view.
+
+## Debug Mode
+
+The debug option in your `config/app.php` configuration file determines how much information about an error is actually displayed to the user. By default, this option is set to respect the value of the `APP_DEBUG` environment variable, which is stored in your application's .env file.
+
+**In your production environment, this value should always be `false`. If the `APP_DEBUG` variable is set to `true` in production, you risk exposing sensitive configuration values to your application's end users.**
diff --git a/docs/Eloquent.md b/docs/Eloquent.md
new file mode 100644
index 00000000..628fcb82
--- /dev/null
+++ b/docs/Eloquent.md
@@ -0,0 +1,269 @@
+# Laravel Eloquent
+
+By default, the SDK does not include any Eloquent models or database support. You can adapt the SDK to your application's needs by adding your own Eloquent models and database support.
+
+## Creating a User Model
+
+Begin by creating a new Eloquent model class. You can use the `make:model` Artisan command to do this. Laravel ships with default user model named `User`, so we'll use the `--force` flag to overwrite it with our custom one.
+
+Please ensure you have a backup of your existing `User` model before running this command, as it will overwrite your existing model.
+
+```bash
+php artisan make:model User --force
+```
+
+Next, open your `app/Models/User.php` file and modify it match the following example. Notably, we'll add a support for a new `auth0` attribute. This attribute will be used to store the user's Auth0 ID, which is used to uniquely identify the user in Auth0.
+
+```php
+ 'datetime',
+ ];
+}
+```
+
+Next, create a migration to update your application's `users` table schema to support these changes. Create a new migration file:
+
+```bash
+php artisan make:migration update_users_table --table=users
+```
+
+Openly the newly created migration file (found under `database/migrations` and ending in `update_users_table.php`) and update to match the following example:
+
+```php
+string('auth0')->nullable();
+ $table->boolean('email_verified')->default(false);
+
+ $table->unique('auth0', 'users_auth0_unique');
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropUnique('users_auth0_unique');
+
+ $table->dropColumn('auth0');
+ $table->dropColumn('email_verified');
+ });
+ }
+};
+
+```
+
+Now run the migration:
+
+```bash
+php artisan migrate
+```
+
+## Creating a User Repository
+
+You'll need to create a new user repository class to handle the creation and retrieval of your Eloquent user models from your database table.
+
+Create a new repository class in your application at `app/Repositories/UserRepository.php`, and update it to match the following example:
+
+```php
+ "https://example.auth0.com/",
+ "aud" => "https://api.example.com/calendar/v1/",
+ "sub" => "auth0|123456",
+ "exp" => 1458872196,
+ "iat" => 1458785796,
+ "scope" => "read write",
+ ];
+ */
+
+ $identifier = $user['sub'] ?? $user['auth0'] ?? null;
+
+ if (null === $identifier) {
+ return null;
+ }
+
+ return User::where('auth0', $identifier)->first();
+ }
+
+ public function fromSession(array $user): ?Authenticatable
+ {
+ /*
+ $user = [ // Example of a decoded ID token
+ "iss" => "http://example.auth0.com",
+ "aud" => "client_id",
+ "sub" => "auth0|123456",
+ "exp" => 1458872196,
+ "iat" => 1458785796,
+ "name" => "Jane Doe",
+ "email" => "janedoe@example.com",
+ ];
+ */
+
+ // Determine the Auth0 identifier for the user from the $user array.
+ $identifier = $user['sub'] ?? $user['auth0'] ?? null;
+
+ // Collect relevant user profile information from the $user array for use later.
+ $profile = [
+ 'auth0' => $identifier,
+ 'name' => $user['name'] ?? '',
+ 'email' => $user['email'] ?? '',
+ 'email_verified' => in_array($user['email_verified'], [1, true], true),
+ ];
+
+ // Check if a cache of the user exists in memory to avoid unnecessary database queries.
+ $cached = $this->withoutRecording(fn () => Cache::get('auth0_user_' . $identifier));
+
+ if ($cached) {
+ // Immediately return a cached user if one exists.
+ return $cached;
+ }
+
+ $user = null;
+
+ // Check if the user exists in the database by Auth0 identifier.
+ if (null !== $identifier) {
+ $user = User::where('auth0', $identifier)->first();
+ }
+
+ // Optional: if the user does not exist in the database by Auth0 identifier, you could fallback to matching by email.
+ if (null === $user && isset($user['email'])) {
+ $user = User::where('email', $user['email'])->first();
+ }
+
+ // If a user was found, check if any updates to the local record are required.
+ if (null !== $user) {
+ $updates = [];
+
+ if ($user->auth0 !== $profile['auth0']) {
+ $updates['auth0'] = $profile['auth0'];
+ }
+
+ if ($user->name !== $profile['name']) {
+ $updates['name'] = $profile['name'];
+ }
+
+ if ($user->email !== $profile['email']) {
+ $updates['email'] = $profile['email'];
+ }
+
+ $emailVerified = in_array($user->email_verified, [1, true], true);
+
+ if ($emailVerified !== $profile['email_verified']) {
+ $updates['email_verified'] = $profile['email_verified'];
+ }
+
+ if ([] !== $updates) {
+ $user->update($updates);
+ $user->save();
+ }
+
+ if ([] === $updates && null !== $cached) {
+ return $user;
+ }
+ }
+
+ if (null === $user) {
+ // Local password column is not necessary or used by Auth0 authentication, but may be expected by some applications/packages.
+ $profile['password'] = Hash::make(Str::random(32));
+
+ // Create the user.
+ $user = User::create($profile);
+ }
+
+ // Cache the user for 30 seconds.
+ $this->withoutRecording(fn () => Cache::put('auth0_user_' . $identifier, $user, 30));
+
+ return $user;
+ }
+
+ /**
+ * Workaround for Laravel Telescope potentially causing an infinite loop.
+ * @link https://github.com/auth0/laravel-auth0/tree/main/docs/Telescope.md
+ *
+ * @param callable $callback
+ */
+ private function withoutRecording($callback): mixed
+ {
+ $telescope = '\Laravel\Telescope\Telescope';
+
+ if (class_exists($telescope)) {
+ return "$telescope"::withoutRecording($callback);
+ }
+
+ return call_user_func($callback);
+ }
+}
+```
+
+Finally, update your application's `config/auth.php` file to configure the SDK to query your new user provider during authentication requests.
+
+```php
+'providers' => [
+ 'auth0-provider' => [
+ 'driver' => 'auth0.provider',
+ 'repository' => \App\Repositories\UserRepository::class,
+ ],
+],
+```
diff --git a/docs/Events.md b/docs/Events.md
new file mode 100644
index 00000000..358f7680
--- /dev/null
+++ b/docs/Events.md
@@ -0,0 +1,102 @@
+# Events
+
+- [Introduction](#introduction)
+- [SDK Controller Events](#sdk-controller-events)
+ - [Login Events](#login-events)
+ - [Callback Events](#callback-events)
+ - [Logout Events](#logout-events)
+- [Deprecated SDK Events](#deprecated-sdk-events)
+ - [Authentication Middleware Events](#authentication-middleware-events)
+ - [Authorization Middleware Events](#authorization-middleware-events)
+
+## Introduction
+
+Your application can listen to events raised by the SDK, and respond to them if desired. For example, you might want to log the user's information to a database when they log in.
+
+To listen for these events, you must first create a listener class within your application. These usually live in your `app/Listeners` directory. The following example shows how to listen for the `Illuminate\Auth\Events\Login` event:
+
+```php
+namespace App\Listeners;
+
+use Illuminate\Auth\Events\Login;
+
+final class LogSuccessfulLogin
+{
+ public function handle(Login $event): void
+ {
+ // Log the event to a database.
+ }
+}
+```
+
+You should also register your event listeners in your application's `app/Providers/EventServiceProvider.php` file, for example:
+
+```php
+use Illuminate\Auth\Events\Login;
+use App\Listeners\LogSuccessfulLogin;
+use Illuminate\Support\Facades\Event;
+
+public function boot(): void
+{
+ Event::listen(
+ Login::class, // The event class.
+ [LogSuccessfulLogin::class, 'handle'] // Your listener class and method.
+ );
+}
+```
+
+You can learn more about working with the Laravel event system in the [Laravel documentation](https://laravel.com/docs/events).
+
+## SDK Controller Events
+
+### Login Events
+
+During user authentication triggered by `Auth0\Laravel\Controllers\LoginController` (the `/login` route, by default) the following events may be raised:
+
+| Event | Description |
+| -------------------------------------- | -------------------------------------------------------------------------------------------- |
+| `Illuminate\Auth\Events\Login` | Raised when a user is logging in. The model of the user is provided with the event. |
+| `Auth0\Laravel\Events\LoginAttempting` | Raised before the login redirect is issued, allowing an opportunity to customize parameters. |
+
+### Callback Events
+
+During user authentication callback triggered by `Auth0\Laravel\Controllers\CallbackController` (the `/callback` route, by default) the following events may be raised:
+
+| Event | Description |
+| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `Illuminate\Auth\Events\Attempting` | Raised when a user is returned to the application after authenticating with Auth0. This is raised before verification of the authentication process begins. |
+| `Illuminate\Auth\Events\Failed` | Raised when authentication with Auth0 failed. The reason is provided with the event as an array. |
+| `Auth0\Laravel\Events\AuthenticationFailed` | Raised when authentication with Auth0 failed. This provides an opportunity to intercept the exception thrown by the middleware, by using the event's `setThrowException()` method to `false`. You can also customize the type of exception thrown using `setException()`. |
+| `Illuminate\Auth\Events\Validated` | Raised when authentication was successful, but immediately before the user's session is established. |
+| `Auth0\Laravel\Events\AuthenticationSucceeded` | Raised when authentication was successful. The model of the authenticated user is provided with the event. |
+
+### Logout Events
+
+During user logout by `Auth0\Laravel\Controllers\LogoutController` (the `/logout` route, by default) the following events may be raised:
+
+| Event | Description |
+| ------------------------------- | ------------------------------------------------------------------------------------ |
+| `Illuminate\Auth\Events\Logout` | Raised when a user is logging out. The model of the user is provided with the event. |
+
+## Deprecated SDK Events
+
+The following events are deprecated and will be removed in a future release. They are replaced by the events listed in the previous section.
+
+### Authentication Middleware Events
+
+During request handling with `Auth0\Laravel\Middleware\AuthenticateMiddleware` or `Auth0\Laravel\Middleware\AuthenticateOptionalMiddleware` the following events may be raised:
+
+| Event | Description |
+| ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
+| `Auth0\Laravel\Events\Middleware\StatefulMiddlewareRequest` | Raised when a request is being handled by a session-based ('stateful') middleware. |
+
+### Authorization Middleware Events
+
+During request handling with `Auth0\Laravel\Middleware\AuthorizeMiddleware` or `Auth0\Laravel\Middleware\AuthorizeOptionalMiddleware` middleware, the following events may be raised:
+
+| Event | Description |
+| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
+| `Auth0\Laravel\Events\Middleware\StatelessMiddlewareRequest` | Raised when a request is being handled by an access token-based ('stateless') middleware. |
+| `Auth0\Laravel\Events\TokenVerificationAttempting` | Raised before an access token is attempted to be verified. The encoded token string is provided with the event. |
+| `Auth0\Laravel\Events\TokenVerificationSucceeded` | Raised when an access token is successfully verified. The decoded token contents are provided with the event. |
+| `Auth0\Laravel\Events\TokenVerificationFailed` | Raised when an access token cannot be verified. The reason (as a string) is provided with the event. |
diff --git a/docs/Installation.md b/docs/Installation.md
new file mode 100644
index 00000000..15808f5e
--- /dev/null
+++ b/docs/Installation.md
@@ -0,0 +1,179 @@
+# Installation
+
+- [Prerequisites](#prerequisites)
+- [Install the SDK](#install-the-sdk)
+ - [Using Quickstart (Recommended)](#using-quickstart-recommended)
+ - [Installation with Composer](#installation-with-composer)
+ - [Create a Laravel Application](#create-a-laravel-application)
+ - [Install the SDK](#install-the-sdk-1)
+- [Install the CLI](#install-the-cli)
+ - [Authenticate the CLI](#authenticate-the-cli)
+- [Configure the SDK](#configure-the-sdk)
+ - [Using JSON (Recommended)](#using-json-recommended)
+ - [Using Environment Variables](#using-environment-variables)
+
+## Prerequisites
+
+Your application must use the [latest supported Laravel version](https://endoflife.date/laravel), and your host environment must be running a [supported PHP version](https://www.php.net/supported-versions.php). Please review [our support policy](./docs/Support.md) for more information. You will also need [Composer 2.0+](https://getcomposer.org/) and an [Auth0 account](https://auth0.com/signup).
+
+## Install the SDK
+
+Ensure that your development environment has [supported versions](#prerequisites) of PHP and [Composer](https://getcomposer.org/) installed. If you're using macOS, PHP and Composer can be installed via [Homebrew](https://brew.sh/). It's also advisable to [install Node and NPM](https://nodejs.org/).
+
+### Using Quickstart (Recommended)
+
+- Create a new Laravel 9 project pre-configured with the SDK:
+
+ ```shell
+ composer create-project auth0-samples/laravel auth0-laravel-app
+ ```
+
+### Installation with Composer
+
+#### Create a Laravel Application
+
+- If you do not already have one, you can Create a new Laravel 9 application with the following command:
+
+ ```shell
+ composer create-project laravel/laravel:^9.0 auth0-laravel-app
+ ```
+
+#### Install the SDK
+
+1. Run the following command from your project directory to install the SDK:
+
+ ```shell
+ composer require auth0/login:^7.8 --update-with-all-dependencies
+ ```
+
+2. Generate an SDK configuration file for your application:
+
+ ```shell
+ php artisan vendor:publish --tag auth0
+ ```
+
+## Install the CLI
+
+Install the [Auth0 CLI](https://github.com/auth0/auth0-cli) to create and manage Auth0 resources from the command line.
+
+- macOS with [Homebrew](https://brew.sh/):
+
+ ```shell
+ brew tap auth0/auth0-cli && brew install auth0
+ ```
+
+- Linux or macOS:
+
+ ```shell
+ curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh -s -- -b .
+ sudo mv ./auth0 /usr/local/bin
+ ```
+
+- Windows with [Scoop](https://scoop.sh/):
+
+ ```cmd
+ scoop bucket add auth0 https://github.com/auth0/scoop-auth0-cli.git
+ scoop install auth0
+ ```
+
+### Authenticate the CLI
+
+- Authenticate the CLI with your Auth0 account. Choose "as a user," and follow the prompts.
+
+ ```shell
+ auth0 login
+ ```
+
+## Configure the SDK
+
+### Using JSON (Recommended)
+
+1. Register a new application with Auth0:
+
+ ```shell
+ auth0 apps create \
+ --name "My Laravel Application" \
+ --type "regular" \
+ --auth-method "post" \
+ --callbacks "http://localhost:8000/callback" \
+ --logout-urls "http://localhost:8000" \
+ --reveal-secrets \
+ --no-input \
+ --json > .auth0.app.json
+ ```
+
+2. Register a new API with Auth0:
+
+ ```shell
+ auth0 apis create \
+ --name "My Laravel Application API" \
+ --identifier "https://github.com/auth0/laravel-auth0" \
+ --offline-access \
+ --no-input \
+ --json > .auth0.api.json
+ ```
+
+3. Add the new files to `.gitignore`:
+
+ Linux and macOS:
+
+ ```bash
+ echo ".auth0.*.json" >> .gitignore
+ ```
+
+ Windows PowerShell:
+
+ ```powershell
+ Add-Content .gitignore "`n.auth0.*.json"
+ ```
+
+ Windows Command Prompt:
+
+ ```cmd
+ echo .auth0.*.json >> .gitignore
+ ```
+
+### Using Environment Variables
+
+1. Register a new application with Auth0:
+
+ ```shell
+ auth0 apps create \
+ --name "My Laravel Application" \
+ --type "regular" \
+ --auth-method "post" \
+ --callbacks "http://localhost:8000/callback" \
+ --logout-urls "http://localhost:8000" \
+ --reveal-secrets \
+ --no-input
+ ```
+
+ Make a note of the `client_id` and `client_secret` values in the output.
+
+2. Register a new API with Auth0:
+
+ ```shell
+ auth0 apis create \
+ --name "My Laravel Application API" \
+ --identifier "https://github.com/auth0/laravel-auth0" \
+ --offline-access \
+ --no-input
+ ```
+
+3. Open the `.env` file found inside your project directory, and add the following lines, replacing the values with the ones you noted in the previous steps:
+
+ ```ini
+ # The Auth0 domain for your tenant (e.g. tenant.region.auth0.com):
+ AUTH0_DOMAIN=...
+
+ # The application `client_id` you noted above:
+ AUTH0_CLIENT_ID=...
+
+ # The application `client_secret` you noted above:
+ AUTH0_CLIENT_SECRET=...
+
+ # The API `identifier` you used above:
+ AUTH0_AUDIENCE=...
+ ```
+
+ Additional configuration environment variables can be found in the [configuration guide](./Configuration.md#environment-variables).
diff --git a/docs/Management.md b/docs/Management.md
new file mode 100644
index 00000000..dae7f262
--- /dev/null
+++ b/docs/Management.md
@@ -0,0 +1,1319 @@
+# Management API
+
+The Auth0 Laravel SDK provides easy-to-use methods to access Auth0's Management API endpoints. Nearly every endpoint of the Management API is available to use with your Laravel application. For more information about any of these endpoints, see the [Management API Explorer](https://auth0.com/docs/api/management/v2).
+
+The Management API class can be accessed through the `management()` method on the Auth0 Laravel SDK service. You can pull the Auth0 SDK instance from the Laravel service container using dependency injection, or use the `Auth0` facade. Once you have an instance, you can call any of the [available endpoints](#available-endpoints).
+
+```php
+use Auth0\Laravel\Facade\Auth0;
+
+Auth0::management();
+```
+
+## API Application Authorization
+
+Before making Management API calls you must permit your application to communicate with the Management API. This can be done from the [Auth0 Dashboard's API page](https://manage.auth0.com/#/apis/), choosing `Auth0 Management API`, and selecting the 'Machine to Machine Applications' tab. Authorize your Laravel application, and then click the down arrow to choose the scopes you wish to grant.
+
+## Available endpoints
+
+- [Actions](#actions)
+- [Attack Protection](#attack-protection)
+- [Blacklists](#blacklists)
+- [ClientGrants](#client-grants)
+- [Clients](#clients)
+- [Connections](#connections)
+- [Device Credentials](#device-credentials)
+- [Emails](#emails)
+- [Email Templates](#email-templates)
+- [Grants](#grants)
+- [Guardian](#guardian)
+- [Jobs](#jobs)
+- [Logs](#logs)
+- [Log Streams](#log-streams)
+- [Organizations](#organizations)
+- [Resource Servers](#resource-servers)
+- [Roles](#roles)
+- [Rules](#rules)
+- [Stats](#stats)
+- [Tenants](#tenants)
+- [Tickets](#tickets)
+- [User Blocks](#user-blocks)
+- [Users](#users)
+- [Users by Email](#users-by-email)
+
+### Actions
+
+The [/api/v2/actions](https://auth0.com/docs/api/management/v2#!/Actions) endpoint class is accessible from the `actions()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
+| `GET` | […/actions/actions](https://auth0.com/docs/api/management/v2#!/Actions/get_actions) | `getAll()` |
+| `POST` | […/actions/actions](https://auth0.com/docs/api/management/v2#!/Actions/post_action) | `create(body: [])` |
+| `GET` | […/actions/actions/{id}](https://auth0.com/docs/api/management/v2#!/Actions/get_action) | `get(id: '...')` |
+| `PATCH` | […/actions/actions/{id}](https://auth0.com/docs/api/management/v2#!/Actions/patch_action) | `update(id: '...', body: [])` |
+| `DELETE` | […/actions/actions/{id}](https://auth0.com/docs/api/management/v2#!/Actions/delete_action) | `delete(id: '...')` |
+| `POST` | […/actions/actions/{id}/test](https://auth0.com/docs/api/management/v2#!/Actions/post_test_action) | `test(id: '...')` |
+| `POST` | […/actions/actions/{id}/deploy](https://auth0.com/docs/api/management/v2#!/Actions/post_deploy_action) | `deploy(id: '...')` |
+| `GET` | […/actions/actions/{actionId}/versions](https://auth0.com/docs/api/management/v2#!/Actions/get_action_versions) | `getVersions(actionId: '...')` |
+| `GET` | […/actions/actions/{actionId}/versions/{id}](https://auth0.com/docs/api/management/v2#!/Actions/get_action_version) | `getVersion(id: '...', actionId: '...')` |
+| `POST` | […/actions/actions/{actionId}/versions/{id}/deploy](https://auth0.com/docs/api/management/v2#!/Actions/post_deploy_draft_version) | `rollbackVersion(id: '...', actionId: '...')` |
+| `GET` | […/actions/executions/{id}](https://auth0.com/docs/api/management/v2#!/Actions/get_execution) | `getExecution(id: '...')` |
+| `GET` | […/actions/triggers](https://auth0.com/docs/api/management/v2#!/Actions/get_triggers) | `getTriggers()` |
+| `GET` | […/actions/triggers/{triggerId}/bindings](https://auth0.com/docs/api/management/v2#!/Actions/get_bindings) | `getTriggerBindings(triggerId: '...')` |
+| `PATCH` | […/actions/triggers/{triggerId}/bindings](https://auth0.com/docs/api/management/v2#!/Actions/patch_bindings) | `updateTriggerBindings(triggerId: '...', body: [])` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Actions class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Actions.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$actions = $management->actions();
+
+// Retrieves the first batch of results results.
+$results = $actions->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($actions->getResponsePaginator() as $action) {
+ // Do something with the action.
+}
+```
+
+### Attack Protection
+
+The [/api/v2/attack-protection](https://auth0.com/docs/api/management/v2#!/Attack_Protection) endpoint class is accessible from the `attackProtection()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
+| `GET` | […/attack-protection/breached-password-detection](https://auth0.com/docs/api/management/v2#!/Attack_Protection/get_breached_password_detection) | `getBreachedPasswordDetection()` |
+| `PATCH` | […/attack-protection/breached-password-detection](https://auth0.com/docs/api/management/v2#!/Attack_Protection/patch_breached_password_detection) | `updateBreachedPasswordDetection(body: [])` |
+| `GET` | […/attack-protection/brute-force-protection](https://auth0.com/docs/api/management/v2#!/Attack_Protection/get_brute_force_protection) | `getBruteForceProtection()` |
+| `PATCH` | […/attack-protection/brute-force-protection](https://auth0.com/docs/api/management/v2#!/Attack_Protection/patch_brute_force_protection) | `updateBruteForceProtection(body: [])` |
+| `GET` | […/attack-protection/suspicious-ip-throttling](https://auth0.com/docs/api/management/v2#!/Attack_Protection/get_suspicious_ip_throttling) | `getSuspiciousIpThrottling()` |
+| `PATCH` | […/attack-protection/suspicious-ip-throttling](https://auth0.com/docs/api/management/v2#!/Attack_Protection/patch_suspicious_ip_throttling) | `updateSuspiciousIpThrottling(body: [])` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\AttackProtection class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/AttackProtection.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$attackProtection = $management->attackProtection();
+
+// Get the current configuration.
+$response = $attackProtection->getBreachedPasswordDetection();
+
+// Print the response body.
+dd(HttpResponse::decode($response));
+
+// {
+// "enabled": true,
+// "shields": [
+// "block",
+// "admin_notification"
+// ],
+// "admin_notification_frequency": [
+// "immediately",
+// "weekly"
+// ],
+// "method": "standard",
+// "stage": {
+// "pre-user-registration": {
+// "shields": [
+// "block",
+// "admin_notification"
+// ]
+// }
+// }
+// }
+```
+
+### Blacklists
+
+The [/api/v2/blacklists](https://auth0.com/docs/api/management/v2#!/Blacklists) endpoint class is accessible from the `blacklists()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| ------ | ---------------------------------------------------------------------------------------- | -------------------- |
+| `GET` | […/blacklists/tokens](https://auth0.com/docs/api/management/v2#!/Blacklists/get_tokens) | `get()` |
+| `POST` | […/blacklists/tokens](https://auth0.com/docs/api/management/v2#!/Blacklists/post_tokens) | `create(jti: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Blacklists class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Blacklists.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$blacklists = $management->blacklists();
+
+$response = $blacklists->create('some-jti');
+
+if ($response->getStatusCode() === 201) {
+ // Token was successfully blacklisted.
+
+ // Retrieve all blacklisted tokens.
+ $results = $blacklists->get();
+
+ // You can then iterate (and auto-paginate) through all available results.
+ foreach ($blacklists->getResponsePaginator() as $blacklistedToken) {
+ // Do something with the blacklisted token.
+ }
+
+ // Or, just work with the initial batch from the response.
+ dd(HttpResponse::decode($results));
+
+ // [
+ // {
+ // "aud": "...",
+ // "jti": "some-jti"
+ // }
+ // ]
+}
+```
+
+### Client Grants
+
+The [/api/v2/client-grants](https://auth0.com/docs/api/management/v2#!/Client_Grants) endpoint class is accessible from the `clientGrants()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
+| `GET` | […/client-grants](https://auth0.com/docs/api/management/v2#!/Client_Grants/get_client_grants) | `getAll()` |
+| `GET` | […/client-grants](https://auth0.com/docs/api/management/v2#!/Client_Grants/get_client_grants) | `getAllByAudience(audience: '...')` |
+| `GET` | […/client-grants](https://auth0.com/docs/api/management/v2#!/Client_Grants/get_client_grants) | `getAllByClientId(clientId: '...')` |
+| `POST` | […/client-grants](https://auth0.com/docs/api/management/v2#!/Client_Grants/post_client_grants) | `create(clientId: '...', audience: '...')` |
+| `PATCH` | […/client-grants/{id}](https://auth0.com/docs/api/management/v2#!/Client_Grants/patch_client_grants_by_id) | `update(grantId: '...')` |
+| `DELETE` | […/client-grants/{id}](https://auth0.com/docs/api/management/v2#!/Client_Grants/delete_client_grants_by_id) | `delete(grantId: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\ClientGrants class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/ClientGrants.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$clientGrants = $management->clientGrants();
+
+// Retrieves the first batch of results results.
+$results = $clientGrants->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($clientGrants->getResponsePaginator() as $clientGrant) {
+ // Do something with the client grant.
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "id": "",
+// "client_id": "",
+// "audience": "",
+// "scope": [
+// ""
+// ]
+// }
+// ]
+```
+
+### Clients
+
+The [/api/v2/clients](https://auth0.com/docs/api/management/v2#!/Clients) endpoint class is accessible from the `clients()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | ----------------------------------------------------------------------------------------- | --------------------- |
+| `GET` | […/clients](https://auth0.com/docs/api/management/v2#!/Clients/get_clients) | `getAll()` |
+| `POST` | […/clients](https://auth0.com/docs/api/management/v2#!/Clients/post_clients) | `create(name: '...')` |
+| `GET` | […/clients/{id}](https://auth0.com/docs/api/management/v2#!/Clients/get_clients_by_id) | `get(id: '...')` |
+| `PATCH` | […/clients/{id}](https://auth0.com/docs/api/management/v2#!/Clients/patch_clients_by_id) | `update(id: '...')` |
+| `DELETE` | […/clients/{id}](https://auth0.com/docs/api/management/v2#!/Clients/delete_clients_by_id) | `delete(id: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Clients class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Clients.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$clients = $management->clients();
+
+// Retrieves the first batch of results results.
+$results = $clients->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($clients->getResponsePaginator() as $client) {
+ // Do something with the client.
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "client_id": "",
+// "tenant": "",
+// "name": "",
+// ...
+// }
+// ]
+```
+
+### Connections
+
+The [/api/v2/connections](https://auth0.com/docs/api/management/v2#!/Connections) endpoint class is accessible from the `connections()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------- |
+| `GET` | […/connections](https://auth0.com/docs/api/management/v2#!/Connections/get_connections) | `getAll()` |
+| `POST` | […/connections](https://auth0.com/docs/api/management/v2#!/Connections/post_connections) | `create(name: '...', strategy: '...')` |
+| `GET` | […/connections/{id}](https://auth0.com/docs/api/management/v2#!/Connections/get_connections_by_id) | `get(id: '...')` |
+| `PATCH` | […/connections/{id}](https://auth0.com/docs/api/management/v2#!/Connections/patch_connections_by_id) | `update(id: '...')` |
+| `DELETE` | […/connections/{id}](https://auth0.com/docs/api/management/v2#!/Connections/delete_connections_by_id) | `delete(id: '...')` |
+| `DELETE` | […/connections/{id}/users](https://auth0.com/docs/api/management/v2#!/Connections/delete_users_by_email) | `deleteUser(id: '...', email: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Connections class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Connections.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$connections = $management->connections();
+
+// Retrieves the first batch of results results.
+$results = $connections->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($connections->getResponsePaginator() as $connection) {
+ // Do something with the connection.
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "name": "",
+// "display_name": "",
+// "options": {},
+// "id": "",
+// "strategy": "",
+// "realms": [
+// ""
+// ],
+// "is_domain_connection": false,
+// "metadata": {}
+// }
+// ]
+```
+
+### Device Credentials
+
+The [/api/v2/device-credentials](https://auth0.com/docs/api/management/v2#!/Device_Credentials) endpoint class is accessible from the `deviceCredentials()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
+| `GET` | […/device-credentials](https://auth0.com/docs/api/management/v2#!/Device_Credentials/get_device_credentials) | `get(userId: '...')` |
+| `POST` | […/device-credentials](https://auth0.com/docs/api/management/v2#!/Device_Credentials/post_device_credentials) | `create(deviceName: '...', type: '...', value: '...', deviceId: '...')` |
+| `DELETE` | […/device-credentials/{id}](https://auth0.com/docs/api/management/v2#!/Device_Credentials/delete_device_credential_by_id) | `delete(id: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\DeviceCredentials class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/DeviceCredentials.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$deviceCredentials = $management->deviceCredentials();
+
+// Retrieves the first batch of results results.
+$results = $deviceCredentials->get('user_id');
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($deviceCredentials->getResponsePaginator() as $deviceCredential) {
+ // Do something with the device credential.
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "id": "",
+// "device_name": "",
+// "device_id": "",
+// "type": "",
+// "user_id": "",
+// "client_id": ""
+// }
+// ]
+```
+
+### Emails
+
+The [/api/v2/emails](https://auth0.com/docs/api/management/v2#!/Emails) endpoint class is accessible from the `emails()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | -------------------------------------------------------------------------------------- | ---------------------------------------------- |
+| `GET` | […/emails/provider](https://auth0.com/docs/api/management/v2#!/Emails/get_provider) | `getProvider()` |
+| `POST` | […/emails/provider](https://auth0.com/docs/api/management/v2#!/Emails/post_provider) | `createProvider(name: '...', credentials: [])` |
+| `PATCH` | […/emails/provider](https://auth0.com/docs/api/management/v2#!/Emails/patch_provider) | `updateProvider(name: '...', credentials: [])` |
+| `DELETE` | […/emails/provider](https://auth0.com/docs/api/management/v2#!/Emails/delete_provider) | `deleteProvider()` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Emails class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Emails.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$endpoint = $management->emails();
+
+// Configure the email provider.
+$endpoint->createProvider(
+ name: 'smtp',
+ credentials: [
+ 'smtp_host' => '...',
+ 'smtp_port' => 587,
+ 'smtp_user' => '...',
+ 'smtp_pass' => '...',
+ ],
+ body: [
+ 'enabled' => true,
+ 'default_from_address' => 'sender@auth0.com',
+ ]
+)
+
+// Retrieves the configuration of the email provider.
+$provider = $endpoint->getProvider();
+
+// Print the configuration.
+dd(HttpResponse::decode($provider));
+
+// {
+// "name": "smtp",
+// "enabled": true,
+// "default_from_address": "sender@auth0.com",
+// "credentials": {
+// 'smtp_host' => '...',
+// 'smtp_port' => 587,
+// 'smtp_user' => '...',
+// 'smtp_pass' => '...',
+// },
+// "settings": {}
+// }
+```
+
+### Email Templates
+
+The [/api/v2/email-templates](https://auth0.com/docs/api/management/v2#!/Email_Templates) endpoint class is accessible from the `emailTemplates()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| ------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
+| `POST` | […/email-templates](https://auth0.com/docs/api/management/v2#!/Email_Templates/post_email_templates) | `create(template: '...', body: '...', from: '...', subject: '...', syntax: '...', enabled: true)` |
+| `GET` | […/email-templates/{templateName}](https://auth0.com/docs/api/management/v2#!/Email_Templates/get_email_templates_by_templateName) | `get(templateName: '...')` |
+| `PATCH` | […/email-templates/{templateName}](https://auth0.com/docs/api/management/v2#!/Email_Templates/patch_email_templates_by_templateName) | `update(templateName: '...', body: [])` |
+| `PUT` | […/email-templates/{templateName}](https://auth0.com/docs/api/management/v2#!/Email_Templates/put_email_templates_by_templateName) | `update(templateName: '...', body: [])` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\EmailTemplates class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/EmailTemplates.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$templates = $management->emailTemplates();
+
+// Create a new email template.
+$templates->create(
+ template: 'verify_email',
+ body: '...',
+ from: 'sender@auth0.com',
+ subject: '...',
+ syntax: 'liquid',
+ enabled: true,
+);
+
+// Retrieves the configuration of the email template.
+$template = $templates->get(templateName: 'verify_email');
+
+// Print the configuration.
+dd(HttpResponse::decode($template));
+
+// {
+// "template": "verify_email",
+// "body": "",
+// "from": "sender@auth0.com",
+// "resultUrl": "",
+// "subject": "",
+// "syntax": "liquid",
+// "urlLifetimeInSeconds": 0,
+// "includeEmailInRedirect": false,
+// "enabled": false
+// }
+```
+
+### Grants
+
+The [/api/v2/grants](https://auth0.com/docs/api/management/v2#!/Grants) endpoint class is accessible from the `grants()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | -------------------------------------------------------------------------------------- | ----------------------------------- |
+| `GET` | […/grants](https://auth0.com/docs/api/management/v2#!/Grants/get_grants) | `getAll()` |
+| `GET` | […/grants](https://auth0.com/docs/api/management/v2#!/Grants/get_grants) | `getAllByAudience(audience: '...')` |
+| `GET` | […/grants](https://auth0.com/docs/api/management/v2#!/Grants/get_grants) | `getAllByClientId(clientId: '...')` |
+| `GET` | […/grants](https://auth0.com/docs/api/management/v2#!/Grants/get_grants) | `getAllByUserId(userId: '...')` |
+| `DELETE` | […/grants/{id}](https://auth0.com/docs/api/management/v2#!/Grants/delete_grants_by_id) | `delete(id: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Grants class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Grants.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$grants = $management->grants();
+
+// Retrieves the first batch of grant results.
+$results = $grants->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($grants->getResponsePaginator() as $grant) {
+ // Do something with the device credential.
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "id": "...",
+// "clientID": "...",
+// "user_id": "...",
+// "audience": "...",
+// "scope": [
+// "..."
+// ],
+// }
+// ]
+```
+
+### Guardian
+
+The [/api/v2/guardian](https://auth0.com/docs/api/management/v2#!/Guardian) endpoint class is accessible from the `guardian()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------- |
+| `GET` | […/guardian/enrollments/{id}](https://auth0.com/docs/api/management/v2#!/Guardian/get_enrollments_by_id) | `getEnrollment(id: '...')` |
+| `DELETE` | […/guardian/enrollments/{id}](https://auth0.com/docs/api/management/v2#!/Guardian/delete_enrollments_by_id) | `deleteEnrollment(id: '...')` |
+| `GET` | […/guardian/factors](https://auth0.com/docs/api/management/v2#!/Guardian/get_factors) | `getFactors()` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Guardian class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Guardian.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$guardian = $management->guardian();
+
+// Retrieves the first batch of factor results.
+$results = $guardian->getFactors();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($guardian->getResponsePaginator() as $factor) {
+ // Do something with the device credential.
+ dump($factor);
+
+ // {
+ // "enabled": true,
+ // "trial_expired": true,
+ // "name": "..."
+ // }
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "enabled": true,
+// "trial_expired": true,
+// "name": "..."
+// }
+// ]
+```
+
+### Jobs
+
+The [/api/v2/jobs](https://auth0.com/docs/api/management/v2#!/Jobs) endpoint class is accessible from the `jobs()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| ------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
+| `GET` | […/jobs/{id}](https://auth0.com/docs/api/management/v2#!/Jobs/get_jobs_by_id) | `get(id: '...')` |
+| `GET` | […/jobs/{id}/errors](https://auth0.com/docs/api/management/v2#!/Jobs/get_errors) | `getErrors(id: '...')` |
+| `POST` | […/jobs/users-exports](https://auth0.com/docs/api/management/v2#!/Jobs/post_users_exports) | `createExportUsersJob(body: [])` |
+| `POST` | […/jobs/users-imports](https://auth0.com/docs/api/management/v2#!/Jobs/post_users_imports) | `createImportUsers(filePath: '...', connectionId: '...')` |
+| `POST` | […/jobs/verification-email](https://auth0.com/docs/api/management/v2#!/Jobs/post_verification_email) | `createSendVerificationEmail(userId: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Jobs class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Jobs.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$connections = $management->connections();
+$jobs = $management->jobs();
+
+// Create a connection.
+$connection = $connections->create([
+ 'name' => 'Test Connection',
+ 'strategy' => 'auth0',
+]);
+
+if (! HttpResponse::wasSuccessful($job)) {
+ throw new \Exception('Connection creation failed.');
+}
+
+$connection = HttpResponse::decode($connection);
+
+// Create a new user export job.
+$response = $jobs->createExportUsersJob([
+ 'format' => 'json',
+ 'fields' => [
+ ['name' => 'user_id'],
+ ['name' => 'name'],
+ ['name' => 'email'],
+ ['name' => 'identities[0].connection', "export_as": "provider"],
+ ['name' => 'user_metadata.some_field'],
+ ],
+ 'connection_id' => $connection['id'],
+]);
+
+if ($response->getStatusCode() === 201) {
+ // The job was created successfully. Retrieve it's ID.
+ $jobId = HttpResponse::decode($response)['id'];
+ $job = null;
+
+ while (true) {
+ // Get the job status.
+ $job = $jobs->get($jobId);
+
+ if (! HttpResponse::wasSuccessful($job)) {
+ $job = null;
+ break;
+ }
+
+ $job = HttpResponse::decode($job);
+
+ // If the job is complete, break out of the loop.
+ if ($job['status'] === 'completed') {
+ break;
+ }
+
+ // If the job has failed, break out of the loop.
+ if ($job['status'] === 'failed') {
+ $job = null
+ break;
+ }
+
+ // Wait 1 second before checking the job status again.
+ sleep(1);
+ }
+
+ if ($job === null) {
+ // The job failed.
+ $errors = $jobs->getErrors($jobId);
+ dd($errors);
+ }
+
+ // The job completed successfully. Do something with the job.
+ dd($job);
+
+ // Delete the connection.
+ $connections->delete($connection['id']);
+}
+```
+
+### Logs
+
+The [/api/v2/logs](https://auth0.com/docs/api/management/v2#!/Logs) endpoint class is accessible from the `logs()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| ------ | ----------------------------------------------------------------------------- | ---------------- |
+| `GET` | […/logs](https://auth0.com/docs/api/management/v2#!/Logs/get_logs) | `getAll()` |
+| `GET` | […/logs/{id}](https://auth0.com/docs/api/management/v2#!/Logs/get_logs_by_id) | `get(id: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Logs class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Logs.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$logs = $management->logs();
+
+// Retrieves the first batch of log results.
+$results = $logs->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($logs->getResponsePaginator() as $log) {
+ // Do something with the log.
+ dump($log);
+
+ // {
+ // "date": "...",
+ // "type": "...",
+ // "description": "..."
+ // }
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "date": "...",
+// "type": "...",
+// "description": "..."
+// }
+// ]
+```
+
+### Log Streams
+
+The [/api/v2/log-streams](https://auth0.com/docs/api/management/v2#!/Log_Streams) endpoint class is accessible from the `logStreams()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------- |
+| `GET` | […/log-streams](https://auth0.com/docs/api/management/v2#!/Log_Streams/get_log_streams) | `getAll()` |
+| `POST` | […/log-streams](https://auth0.com/docs/api/management/v2#!/Log_Streams/post_log_streams) | `create(type: '...', sink: '...')` |
+| `GET` | […/log-streams/{id}](https://auth0.com/docs/api/management/v2#!/Log_Streams/get_log_streams_by_id) | `get(id: '...')` |
+| `PATCH` | […/log-streams/{id}](https://auth0.com/docs/api/management/v2#!/Log_Streams/patch_log_streams_by_id) | `update(id: '...', body: [])` |
+| `DELETE` | […/log-streams/{id}](https://auth0.com/docs/api/management/v2#!/Log_Streams/delete_log_streams_by_id) | `delete(id: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\LogStreams class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/LogStreams.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$logStreams = $management->logStreams();
+
+// Create a new log stream.
+$logStreams->create(
+ type: '...',
+ sink: [
+ 'name' => '...',
+ ]
+);
+
+// Get the first batch of log streams.
+$results = $logStreams->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($logStreams->getResponsePaginator() as $logStream) {
+ // Do something with the log stream.
+ dump($logStream);
+
+ // {
+ // "id": "...",
+ // "name": "...",
+ // "type": "...",
+ // "status": "..."
+ // }
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "id": "...",
+// "name": "...",
+// "type": "...",
+// "status": "..."
+// }
+// ]
+```
+
+### Organizations
+
+The [/api/v2/organizations](https://auth0.com/docs/api/management/v2#!/Organizations) endpoint class is accessible from the `organizations()` method on the Management API class.
+
+| Method | Endpoint | SDK Method |
+| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
+| `GET` | […/organizations](https://auth0.com/docs/api/management/v2#!/Organizations/get_organizations) | `getAll()` |
+| `POST` | […/organizations](https://auth0.com/docs/api/management/v2#!/Organizations/post_organizations) | `create(name: '...', displayName: '...')` |
+| `GET` | […/organizations/{id}](https://auth0.com/docs/api/management/v2#!/Organizations/get_organizations_by_id) | `get(id: '...')` |
+| `PATCH` | […/organizations/{id}](https://auth0.com/docs/api/management/v2#!/Organizations/patch_organizations_by_id) | `update(id: '...', name: '...', displayName: '...')` |
+| `DELETE` | […/organizations/{id}](https://auth0.com/docs/api/management/v2#!/Organizations/delete_organizations_by_id) | `delete(id: '...')` |
+| `GET` | […/organizations/name/{name}](https://auth0.com/docs/api/management/v2#!/Organizations/get_name_by_name) | `getByName(name: '...')` |
+| `GET` | […/organizations/{id}/members](https://auth0.com/docs/api/management/v2#!/Organizations/get_members) | `getMembers(id: '...')` |
+| `POST` | […/organizations/{id}/members](https://auth0.com/docs/api/management/v2#!/Organizations/post_members) | `addMembers(id: '...', members: [])` |
+| `DELETE` | […/organizations/{id}/members](https://auth0.com/docs/api/management/v2#!/Organizations/delete_members) | `removeMembers(id: '...', members: [])` |
+| `GET` | […/organizations/{id}/invitations](https://auth0.com/docs/api/management/v2#!/Organizations/get_invitations) | `getInvitations(id: '...')` |
+| `POST` | […/organizations/{id}/invitations](https://auth0.com/docs/api/management/v2#!/Organizations/post_invitations) | `createInvitation(id: '...', clientId: '...', inviter: '...', invitee: '...')` |
+| `GET` | […/organizations/{id}/invitations/{invitationId}](https://auth0.com/docs/api/management/v2#!/Organizations/get_invitations_by_invitation_id) | `getInvitation(id: '...', invitationId: '...')` |
+| `DELETE` | […/organizations/{id}/invitations/{invitationId}](https://auth0.com/docs/api/management/v2#!/Organizations/delete_invitations_by_invitation_id) | `deleteInvitation(id: '...', invitationId: '...')` |
+| `GET` | […/organizations/{id}/enabled_connections](https://auth0.com/docs/api/management/v2#!/Organizations/get_enabled_connections) | `getEnabledConnections(id: '...')` |
+| `POST` | […/organizations/{id}/enabled_connections](https://auth0.com/docs/api/management/v2#!/Organizations/post_enabled_connections) | `addEnabledConnection(id: '...', connectionId: '...', body: [])` |
+| `GET` | […/organizations/{id}/enabled_connections/{connectionId}](https://auth0.com/docs/api/management/v2#!/Organizations/get_enabled_connections_by_connectionId) | `getEnabledConnection(id: '...', connectionId: '...')` |
+| `PATCH` | […/organizations/{id}/enabled_connections/{connectionId}](https://auth0.com/docs/api/management/v2#!/Organizations/patch_enabled_connections_by_connectionId) | `updateEnabledConnection(id: '...' connectionId: '...', body: [])` |
+| `DELETE` | […/organizations/{id}/enabled_connections/{connectionId}](https://auth0.com/docs/api/management/v2#!/Organizations/delete_enabled_connections_by_connectionId) | `removeEnabledConnection(id: '...', connectionId: '...')` |
+| `GET` | […/organizations/{id}/members/{userId}/roles](https://auth0.com/docs/api/management/v2#!/Organizations/get_organization_member_roles) | `getMemberRoles(id: '...'. userId: '...')` |
+| `POST` | […/organizations/{id}/members/{userId}/roles](https://auth0.com/docs/api/management/v2#!/Organizations/post_organization_member_roles) | `addMemberRoles(id: '...'. userId: '...', roles: [])` |
+| `DELETE` | […/organizations/{id}/members/{userId}/roles](https://auth0.com/docs/api/management/v2#!/Organizations/delete_organization_member_roles) | `removeMemberRoles(id: '...'. userId: '...', roles: [])` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Organizations class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Organizations.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$organizations = $management->organizations();
+
+// Get all organizations.
+$results = $organizations->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($logStreams->getResponsePaginator() as $logStream) {
+ // Do something with the log stream.
+ dump($logStream);
+
+ // {
+ // "id": "",
+ // "name": "...",
+ // "display_name": "...",
+ // }
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "id": "",
+// "name": "...",
+// "display_name": "...",
+// ]
+
+// Get a single organization.
+$results = $organizations->get('org_id');
+
+// Create a new organization.
+$results = $organizations->create('name', 'display_name');
+
+// Update an existing organization.
+$results = $organizations->update('org_id', 'name', 'display_name');
+
+// Delete an organization.
+$results = $organizations->delete('org_id');
+
+// Get all members of an organization.
+$results = $organizations->getMembers('org_id');
+
+// Add members to an organization.
+$results = $organizations->addMembers('org_id', ['user_id_1', 'user_id_2']);
+
+// Remove members from an organization.
+$results = $organizations->removeMembers('org_id', ['user_id_1', 'user_id_2']);
+
+// Get all invitations for an organization.
+$results = $organizations->getInvitations('org_id');
+
+// Create a new invitation for an organization.
+$results = $organizations->createInvitation('org_id', 'client_id', 'inviter_user_id', 'invitee_email');
+
+// Get a single invitation for an organization.
+$results = $organizations->getInvitation('org_id', 'invitation_id');
+
+// Delete an invitation for an organization.
+$results = $organizations->deleteInvitation('org_id', 'invitation_id');
+
+// Get all enabled connections for an organization.
+$results = $organizations->getEnabledConnections('org_id');
+
+// Add a connection to an organization.
+$results = $organizations->addEnabledConnection('org_id', 'connection_id', ['assign_membership_on_login' => true]);
+
+// Get a single enabled connection for an organization.
+$results = $organizations->getEnabledConnection('org_id', 'connection_id');
+
+// Update an enabled connection for an organization.
+$results = $organizations->updateEnabledConnection('org_id', 'connection_id', ['assign_membership_on_login' => false]);
+
+// Remove a connection from an organization.
+$results = $organizations->removeEnabledConnection('org_id', 'connection_id');
+
+// Get all roles for a member of an organization.
+$results = $organizations->getMemberRoles('org_id', 'user_id');
+
+// Add roles to a member of an organization.
+$results = $organizations->addMemberRoles('org_id', 'user_id', ['role_id_1', 'role_id_2']);
+
+// Remove roles from a member of an organization.
+$results = $organizations->removeMemberRoles('org_id', 'user_id', ['role_id_1', 'role_id_2']);
+```
+
+### Resource Servers
+
+The [/api/v2/resource-servers](https://auth0.com/docs/api/management/v2#!/Resource_Servers) endpoint class is accessible from the `resourceServers()` method on the Management API class.
+
+| Method | Endpoint | PHP Method |
+| -------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
+| `GET` | […/resource-servers](https://auth0.com/docs/api/management/v2#!/Resource_Servers/get_resource_servers) | `getAll()` |
+| `POST` | […/resource-servers](https://auth0.com/docs/api/management/v2#!/Resource_Servers/post_resource_servers) | `create(identifier: '...', body: [])` |
+| `GET` | […/resource-servers/{id}](https://auth0.com/docs/api/management/v2#!/Resource_Servers/get_resource_servers_by_id) | `get(id: '...')` |
+| `PATCH` | […/resource-servers/{id}](https://auth0.com/docs/api/management/v2#!/Resource_Servers/patch_resource_servers_by_id) | `update(id: '...', body: '...')` |
+| `DELETE` | […/resource-servers/{id}](https://auth0.com/docs/api/management/v2#!/Resource_Servers/delete_resource_servers_by_id) | `delete(id: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\ResourceServers class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/ResourceServers.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$resourceServers = $management->resourceServers();
+
+// Create a new resource server.
+$resourceServers->create(
+ identifier: 'https://my-resource-server.auth0.com',
+ body: [
+ 'name' => 'My Example API',
+ 'scopes' => [
+ [
+ 'value' => 'read:messages',
+ 'description' => 'Read messages',
+ ],
+ [
+ 'value' => 'write:messages',
+ 'description' => 'Write messages',
+ ],
+ ],
+ ]
+);
+
+// Get all resource servers.
+$results = $resourceServers->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($logStreams->getResponsePaginator() as $logStream) {
+ // Do something with the log stream.
+ dump($logStream);
+
+ // {
+ // "id": "",
+ // "name": "",
+ // "is_system": false,
+ // "identifier": "",
+ // "scopes": [
+ // "object"
+ // ],
+ // "signing_alg": "",
+ // "signing_secret": "",
+ // "allow_offline_access": false,
+ // "skip_consent_for_verifiable_first_party_clients": false,
+ // "token_lifetime": 0,
+ // "token_lifetime_for_web": 0,
+ // "enforce_policies": false,
+ // "token_dialect": "",
+ // "client": {}
+ // }
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "id": "",
+// "name": "",
+// "is_system": false,
+// "identifier": "",
+// "scopes": [
+// "object"
+// ],
+// "signing_alg": "",
+// "signing_secret": "",
+// "allow_offline_access": false,
+// "skip_consent_for_verifiable_first_party_clients": false,
+// "token_lifetime": 0,
+// "token_lifetime_for_web": 0,
+// "enforce_policies": false,
+// "token_dialect": "",
+// "client": {}
+// }
+// ]
+```
+
+### Roles
+
+The [/api/v2/roles](https://auth0.com/docs/api/management/v2#!/Roles) endpoint class is accessible from the `roles()` method on the Management API class.
+
+| Method | Endpoint | PHP Method |
+| -------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
+| `GET` | […/roles](https://auth0.com/docs/api/management/v2#!/Roles/get_roles) | `getAll()` |
+| `POST` | […/roles](https://auth0.com/docs/api/management/v2#!/Roles/post_roles) | `create(name: '...', body: [])` |
+| `GET` | […/roles/{id}](https://auth0.com/docs/api/management/v2#!/Roles/get_roles_by_id) | `get(id: '...')` |
+| `PATCH` | […/roles/{id}](https://auth0.com/docs/api/management/v2#!/Roles/patch_roles_by_id) | `update(id: '...', body: [])` |
+| `DELETE` | […/roles/{id}](https://auth0.com/docs/api/management/v2#!/Roles/delete_roles_by_id) | `delete(id: '...')` |
+| `GET` | […/roles/{id}/users](https://auth0.com/docs/api/management/v2#!/Roles/get_role_user) | `getUsers(id: '...')` |
+| `POST` | […/roles/{id}/users](https://auth0.com/docs/api/management/v2#!/Roles/post_role_users) | `addUsers(id: '...', users: [])` |
+| `GET` | […/roles/{id}/permissions](https://auth0.com/docs/api/management/v2#!/Roles/get_role_permission) | `getPermissions(id: '...')` |
+| `POST` | […/roles/{id}/permissions](https://auth0.com/docs/api/management/v2#!/Roles/post_role_permission_assignment) | `addPermissions(id: '...', permissions: [])` |
+| `DELETE` | […/roles/{id}/permissions](https://auth0.com/docs/api/management/v2#!/Roles/delete_role_permission_assignment) | `removePermissions(id: '...', permissions: [])` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Roles class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Roles.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$roles = $management->roles();
+
+// Create a new role.
+$roles->create(
+ name: 'My Example Role',
+ body: [
+ 'description' => 'This is an example role.',
+ ]
+);
+
+// Get all roles.
+$results = $roles->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($logStreams->getResponsePaginator() as $logStream) {
+ // Do something with the log stream.
+ dump($logStream);
+
+ // {
+ // "id": "",
+ // "name": "",
+ // "description": "",
+ // }
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "id": "",
+// "name": "",
+// "description": "",
+// }
+// ]
+```
+
+### Rules
+
+The [/api/v2/rules](https://auth0.com/docs/api/management/v2#!/Rules) endpoint class is accessible from the `rules()` method on the Management API class.
+
+| Method | Endpoint | PHP Method |
+| -------- | ----------------------------------------------------------------------------------- | ------------------------------------ |
+| `GET` | […/rules](https://auth0.com/docs/api/management/v2#!/Rules/get_rules) | `getAll()` |
+| `POST` | […/rules](https://auth0.com/docs/api/management/v2#!/Rules/post_rules) | `create(name: '...', script: '...')` |
+| `GET` | […/rules/{id}](https://auth0.com/docs/api/management/v2#!/Rules/get_rules_by_id) | `get(id: '...')` |
+| `PATCH` | […/rules/{id}](https://auth0.com/docs/api/management/v2#!/Rules/patch_rules_by_id) | `update(id: '...', body: [])` |
+| `DELETE` | […/rules/{id}](https://auth0.com/docs/api/management/v2#!/Rules/delete_rules_by_id) | `delete(id: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Rules class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Rules.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$rules = $management->rules();
+
+// Create a new rule.
+$rules->create(
+ name: 'My Example Rule',
+ script: 'function (user, context, callback) { callback(null, user, context); }'
+);
+
+// Get all rules.
+$results = $rules->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($logStreams->getResponsePaginator() as $logStream) {
+ // Do something with the log stream.
+ dump($logStream);
+
+ // {
+ // "id": "",
+ // "name": "",
+ // "script": "",
+ // "enabled": true,
+ // "order": 0,
+ // "stage": "login_success",
+ // }
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "id": "",
+// "name": "",
+// "script": "",
+// "enabled": true,
+// "order": 0,
+// "stage": "login_success",
+// }
+// ]
+```
+
+### Stats
+
+The [/api/v2/stats](https://auth0.com/docs/api/management/v2#!/Stats) endpoint class is accessible from the `stats()` method on the Management API class.
+
+| Method | Endpoint | PHP Method |
+| ------ | ----------------------------------------------------------------------------------------- | --------------------------------- |
+| `GET` | […/stats/active-users](https://auth0.com/docs/api/management/v2#!/Stats/get_active_users) | `getActiveUsers()` |
+| `GET` | […/stats/daily](https://auth0.com/docs/api/management/v2#!/Stats/get_active_users) | `getDaily(from: '...', to: '...)` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Stats class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Stats.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$stats = $management->stats();
+
+// Retrieve the number of logins, signups and breached-password detections (subscription required) that occurred each day within a specified date range.
+$results = $stats->getDaily();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($stats->getResponsePaginator() as $metrics) {
+ // Do something with the log stream.
+ dump($logStream);
+
+ // {
+ // "date": "...",
+ // "logins": 0,
+ // "signups": 0,
+ // "leaked_passwords": 0,
+ // "updated_at": "...",
+ // "created_at": "..."
+ // }
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "date": "...",
+// "logins": 0,
+// "signups": 0,
+// "leaked_passwords": 0,
+// "updated_at": "...",
+// "created_at": "..."
+// }
+// ]
+```
+
+### Tenants
+
+The [/api/v2/tenants](https://auth0.com/docs/api/management/v2#!/Tenants) endpoint class is accessible from the `tenants()` method on the Management API class.
+
+| Method | Endpoint | PHP Method |
+| ------- | --------------------------------------------------------------------------------------- | -------------------------- |
+| `GET` | […/tenants/settings](https://auth0.com/docs/api/management/v2#!/Tenants/get_settings) | `getSettings()` |
+| `PATCH` | […/tenants/settings](https://auth0.com/docs/api/management/v2#!/Tenants/patch_settings) | `updateSettings(body: [])` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Tenants class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Tenants.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$tenants = $management->tenants();
+
+// Retrieve the current tenant settings.
+$results = $tenants->getSettings();
+
+dd(HttpResponse::decode($results));
+
+// {
+// "change_password": {
+// "enabled": false,
+// "html": ""
+// },
+// ...
+// ...
+// ...
+// }
+```
+
+### Tickets
+
+The [/api/v2/tickets](https://auth0.com/docs/api/management/v2#!/Tickets) endpoint class is accessible from the `tickets()` method on the Management API class.
+
+| Method | Endpoint | PHP Method |
+| ------ | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
+| `POST` | […/tickets/password-change](https://auth0.com/docs/api/management/v2#!/Tickets/post_password_change) | `createPasswordChange(body: [])` |
+| `POST` | […/tickets/email-verification](https://auth0.com/docs/api/management/v2#!/Tickets/post_email_verification) | `createEmailVerification(userId: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\Tickets class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/Tickets.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$tickets = $management->tickets();
+
+// Create a password change ticket.
+$results = $tickets->createPasswordChange([
+ 'result_url' => 'https://example.com',
+ 'user_id' => '...',
+ 'client_id' => '...',
+ 'organization_id' => '...',
+ 'connection_id' => '...',
+ 'email' => '...',
+ 'ttl_sec' => 3600,
+ 'mark_email_as_verified' => true,
+ 'includeEmailInRedirect' => true,
+]);
+
+dd(HttpResponse::decode($results));
+
+// {
+// "ticket": "https://login.auth0.com/lo/reset?..."
+// }
+```
+
+### User Blocks
+
+The [/api/v2/user-blocks](https://auth0.com/docs/api/management/v2#!/User_Blocks) endpoint class is accessible from the `userBlocks()` method on the Management API class.
+
+| Method | Endpoint | PHP Method |
+| -------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------- |
+| `GET` | […/user-blocks](https://auth0.com/docs/api/management/v2#!/User_Blocks/get_user_blocks) | `get(id: '...')` |
+| `DELETE` | […/user-blocks](https://auth0.com/docs/api/management/v2#!/User_Blocks/delete_user_blocks) | `delete(id: '...')` |
+| `GET` | […/user-blocks/{id}](https://auth0.com/docs/api/management/v2#!/User_Blocks/get_user_blocks_by_id) | `getByIdentifier(identifier: '...')` |
+| `DELETE` | […/user-blocks/{id}](https://auth0.com/docs/api/management/v2#!/User_Blocks/delete_user_blocks_by_id) | `deleteByIdentifier(identifier: '...')` |
+
+For full usage reference of the available API methods please [review the Auth0\SDK\API\Management\UserBlocks class.](https://github.com/auth0/auth0-PHP/blob/main/src/API/Management/UserBlocks.php)
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$userBlocks = $management->userBlocks();
+
+// Retrieve a list of all user blocks.
+$results = $userBlocks->get('...');
+
+dd(HttpResponse::decode($results));
+
+// {
+// "blocked_for": [
+// {
+// "identifier": "...",
+// "ip": "..."
+// }
+// ]
+// }
+```
+
+### Users
+
+The [/api/v2/users](https://auth0.com/docs/api/management/v2#!/Users) endpoint class is accessible from the `users()` method on the Management API class.
+
+| Method | Endpoint | PHP Method |
+| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
+| `GET` | […/users](https://auth0.com/docs/api/management/v2#!/Users/get_users) | `getAll()` |
+| `POST` | […/users](https://auth0.com/docs/api/management/v2#!/Users/post_users) | `create(connection: '...', body: [])` |
+| `GET` | […/users/{id}](https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id) | `get(id: '...')` |
+| `PATCH` | […/users/{id}](https://auth0.com/docs/api/management/v2#!/Users/patch_users_by_id) | `update(id: '...', body: [])` |
+| `DELETE` | […/users/{id}](https://auth0.com/docs/api/management/v2#!/Users/delete_users_by_id) | `delete(id: '...')` |
+| `GET` | […/users/{id}/enrollments](https://auth0.com/docs/api/management/v2#!/Users/get_enrollments) | `getEnrollments(id: '...')` |
+| `GET` | […/users/{user}/authentication-methods](https://auth0.com/docs/api/management/v2#!/Users/get_authentication_methods) | `getAuthenticationMethods(user: '...')` |
+| `DELETE` | […/users/{user}/authentication-methods](https://auth0.com/docs/api/management/v2#!/Users/delete_authentication_methods) | `deleteAuthenticationMethods(user: '...')` |
+| `POST` | […/users/{user}/authentication-methods](https://auth0.com/docs/api/management/v2#!/Users/post_authentication_methods) | `createAuthenticationMethod(user: '...', body: [])` |
+| `GET` | […/users/{id}/authentication-methods/{method}](https://auth0.com/docs/api/management/v2#!/Users/get_authentication_methods_by_authentication_method_id) | `getAuthenticationMethod(id: '...', method: '...')` |
+| `PATCH` | […/users/{id}/authentication-methods/{method}](https://auth0.com/docs/api/management/v2#!/Users/patch_authentication_methods_by_authentication_method_id) | `updateAuthenticationMethod(id: '...', method: '...', body: [])` |
+| `DELETE` | […/users/{id}/authentication-methods/{method}](https://auth0.com/docs/api/management/v2#!/Users/delete_authentication_methods_by_authentication_method_id) | `deleteAuthenticationMethod(id: '...', method: '...')` |
+| `GET` | […/users/{id}/organizations](https://auth0.com/docs/api/management/v2#!/Users/get_user_organizations) | `getOrganizations(id: '...')` |
+| `GET` | […/users/{id}/logs](https://auth0.com/docs/api/management/v2#!/Users/get_logs_by_user) | `getLogs(id: '...')` |
+| `GET` | […/users/{id}/roles](https://auth0.com/docs/api/management/v2#!/Users/get_user_roles) | `getRoles(id: '...')` |
+| `POST` | […/users/{id}/roles](https://auth0.com/docs/api/management/v2#!/Users/post_user_roles) | `addRoles(id: '...', roles: [])` |
+| `DELETE` | […/users/{id}/roles](https://auth0.com/docs/api/management/v2#!/Users/delete_user_roles) | `removeRoles(id: '...', roles: [])` |
+| `GET` | […/users/{id}/permissions](https://auth0.com/docs/api/management/v2#!/Users/get_permissions) | `getPermissions(id: '...')` |
+| `POST` | […/users/{id}/permissions](https://auth0.com/docs/api/management/v2#!/Users/post_permissions) | `addPermissions(id: '...', permissions: [])` |
+| `DELETE` | […/users/{id}/permissions](https://auth0.com/docs/api/management/v2#!/Users/delete_permissions) | `removePermissions(id: '...', permissions: [])` |
+| `DELETE` | […/users/{id}/multifactor/{provider}](https://auth0.com/docs/api/management/v2#!/Users/delete_multifactor_by_provider) | `deleteMultifactorProvider(id: '...', provider: '...')` |
+| `POST` | […/users/{id}/identities](https://auth0.com/docs/api/management/v2#!/Users/post_identities) | `linkAccount(id: '...', body: [])` |
+| `DELETE` | […/users/{id}/identities/{provider}/{identityId}](https://auth0.com/docs/api/management/v2#!/Users/delete_provider_by_user_id) | `unlinkAccount(id: '...', provider: '...', identityId: '...')` |
+| `POST` | […/users/{id}/recovery-code-regeneration](https://auth0.com/docs/api/management/v2#!/Users/post_recovery_code_regeneration) | `createRecoveryCode(id: '...')` |
+| `POST` | […/users/{id}/multifactor/actions/invalidate-remember-browser](https://auth0.com/docs/api/management/v2#!/Users/post_invalidate_remember_browser) | `invalidateBrowsers(id: '...')` |
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$users = $management->users();
+
+// Create a new user.
+$users->create(
+ connection: 'Username-Password-Authentication',
+ body: [
+ 'email' => '...',
+ 'password' => '...',
+ 'email_verified' => true,
+ ]
+);
+
+// Get a single user.
+$result = $users->get('auth0|...');
+
+dump(HttpResponse::decodedBody($result));
+
+// Get all users.
+$results = $users->getAll();
+
+// You can then iterate (and auto-paginate) through all available results.
+foreach ($users->getResponsePaginator() as $user) {
+ dump($user);
+
+// {
+// "user_id": "...",
+// "email": "...",
+// "email_verified": true,
+// ...
+// }
+}
+
+// Or, just work with the initial batch from the response.
+dd(HttpResponse::decode($results));
+
+// [
+// {
+// "user_id": "...",
+// "email": "...",
+// "email_verified": true,
+// ...
+// }
+// ]
+```
+
+# Users by Email
+
+The `Auth0\SDK\API\Management\UsersByEmail` class provides methods to access the [Users by Email endpoint](https://auth0.com/docs/api/management/v2#!/Users_By_Email) of the v2 Management API.
+
+| Method | Endpoint | PHP Method |
+| ------ | ------------------------------------------------------------------------------------------------ | ---------- |
+| `GET` | […/users-by-email](https://auth0.com/docs/api/management/v2#!/Users_By_Email/get_users_by_email) | `get()` |
+
+```php
+use Auth0\SDK\Utility\HttpResponse;
+
+$management = app('auth0')->management();
+$usersByEmail = $management->usersByEmail();
+
+// Get a single user by email.
+$result = $usersByEmail->get('...');
+
+dump(HttpResponse::decodedBody($result));
+
+// {
+// "user_id": "...",
+// "email": "...",
+// "email_verified": true,
+// ...
+// }
+```
diff --git a/docs/Octane.md b/docs/Octane.md
new file mode 100644
index 00000000..2e868031
--- /dev/null
+++ b/docs/Octane.md
@@ -0,0 +1,7 @@
+# Octane Support
+
+Octane compatibility with the SDK is currently considered experimental and is not supported.
+
+Although we are working toward ensuring the SDK has full compatibility in the future, we do not recommend using this with our SDK in production until we have full confidence and announced support. There is an opportunity for problems we have not fully identified or addressed yet.
+
+Feedback and bug-fix contributions are greatly appreciated as we work toward full support.
diff --git a/docs/Sessions.md b/docs/Sessions.md
new file mode 100644
index 00000000..ef43f89e
--- /dev/null
+++ b/docs/Sessions.md
@@ -0,0 +1,41 @@
+# Sessions
+
+In order to persist users' authentication states between HTTP requests, the Auth0 Laravel SDK uses Laravel's Session API to store and retrieve necessary data about the user. Applications can configure Laravel's Session API by modifying their `config/session.php` file. By default, sessions use the `file` driver, which stores the serialized user information in a file on the application server. However, you can configure the session store to use any of the other session drivers, such as `cookie`, `database`, `apc`, `memcached` and `redis`.
+
+It's important to note that all session drivers, except for `cookie`, require server-side storage of the session data. If you are using a load balancer or other server cluster, you must use a session driver that is shared across all of the servers.
+
+We strongly recommend using the `database` or `redis` session drivers for applications that use Auth0. These drivers are the most reliable and scalable options for storing session data.
+
+## Files
+
+The default session driver is `file`, which stores the session data in files on the server. It works well for simple applications, but it does not scale reliably beyond a single server.
+
+## Cookies
+
+The `cookie` session driver stores the session data in secure, encrypted cookies on the client device. Although convenient, this approach is not a reliable option for production applications as it suffers from a number of notable drawbacks:
+
+- Browsers impose a size limit of 4 KB on individual cookies, which can quickly be exceeded by storing session data.
+- Laravel's cookie driver unfortunately does not "chunk" (split up) larger cookies into multiple cookies, so it is impossible to store more than the noted 4 KB of total session data.
+- Most web servers and load balancers require additional configuration to accept and deliver larger cookie headers.
+
+If your application requires the use of cookies, please use the Auth0 PHP SDK's custom cookie session handler instead. This approach supports chunking of larger cookies, but is notably incompatible with [Octane](./Octane.md). Please refer to [Cookies.md](./Cookies.md) for more information.
+
+## Database
+
+The `database` session driver stores the session data in a database table. This is a very reliable option for applications of any size, but it does require a database connection to be configured for your application.
+
+## Redis
+
+The `redis` session driver stores the session data in a Redis database. This is an equally reliable option to the `database` driver.
+
+## APC
+
+The `apc` session driver stores the session data in the APC cache. This is a very fast and reliable option for applications of any size, but it does require the APC PHP extension to be installed on your server.
+
+## Memcached
+
+The `memcached` session driver stores the session data in a Memcached database. This is an equally reliable option to the `apc` driver, but it does require the Memcached PHP extension to be installed on your server.
+
+## Array (Testing)
+
+The `array` session driver stores the session data in a PHP array. This option is generally used for running tests on your application as it does not persist session data between requests.
diff --git a/docs/Support.md b/docs/Support.md
new file mode 100644
index 00000000..7a1251b7
--- /dev/null
+++ b/docs/Support.md
@@ -0,0 +1,52 @@
+# Support
+
+Your application must use a [supported Laravel version](#supported-laravel-releases), and your host environment must be running a [maintained PHP version](https://www.php.net/supported-versions.php).
+
+You will also need [Composer](https://getcomposer.org/) and an [Auth0 account](https://auth0.com/signup).
+
+### Supported Laravel Releases
+
+The next major release of Laravel is forecasted for Q1 2025. We anticipate supporting it upon release.
+
+| Laravel | SDK | PHP | Supported Until |
+| ---------------------------------------------- | ----- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ |
+| [11.x](https://laravel.com/docs/11.x/releases) | 7.13+ | [8.3](https://www.php.net/releases/8.3/en.php) | Approx. [March 2026](https://laravel.com/docs/11.x/releases#support-policy) (EOL for Laravel 11) |
+| | | [8.2](https://www.php.net/releases/8.2/en.php) | Approx. [Dec 2025](https://www.php.net/supported-versions.php) (EOL for PHP 8.2) |
+
+We strive to support all actively maintained Laravel releases, prioritizing support for the latest major version with our SDK. If a new Laravel major introduces breaking changes, we may have to end support for past Laravel versions earlier than planned.
+
+Affected Laravel versions will still receive security fixes until their end-of-life date, as announced in our release notes.
+
+### Maintenance Releases
+
+The following releases are no longer being updated with new features by Auth0, but will continue to receive security updates through their end-of-life date.
+
+| Laravel | SDK | PHP | Security Fixes Until |
+| ---------------------------------------------- | ---------- | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
+| [10.x](https://laravel.com/docs/10.x/releases) | 7.5 - 7.12 | [8.3](https://www.php.net/releases/8.3/en.php) | [Feb 2025](https://laravel.com/docs/10.x/releases#support-policy) (EOL for Laravel 10) |
+| | | [8.2](https://www.php.net/releases/8.2/en.php) | [Feb 2025](https://laravel.com/docs/10.x/releases#support-policy) (EOL for Laravel 10) |
+| | | [8.1](https://www.php.net/releases/8.2/en.php) | [Nov 2024](https://www.php.net/supported-versions.php) (EOL for PHP 8.1) |
+
+### Unsupported Releases
+
+The following releases are unsupported by Auth0. While they may be suitable for some legacy applications, your mileage may vary. We recommend upgrading to a supported version as soon as possible.
+
+| Laravel | SDK |
+| -------------------------------------------- | ---------- |
+| [9.x](https://laravel.com/docs/9.x/releases) | 7.0 - 7.12 |
+| [8.x](https://laravel.com/docs/8.x/releases) | 7.0 - 7.4 |
+| [7.x](https://laravel.com/docs/7.x/releases) | 5.4 - 6.5 |
+| [6.x](https://laravel.com/docs/6.x/releases) | 5.3 - 6.5 |
+| [5.x](https://laravel.com/docs/5.x/releases) | 2.0 - 6.1 |
+| [4.x](https://laravel.com/docs/4.x/releases) | 1.x |
+
+## Support Policy
+
+The SDK follows the [Laravel support policy](https://laravel.com/docs/master/releases#support-policy) and will be supported until the Laravel version it supports reaches end-of-life, or it is no longer technically feasible to support.
+
+## Getting Support
+
+- If you believe you've found a bug, please [create an issue on GitHub](https://github.com/auth0/laravel-auth0).
+- For questions and community support, please [join the Auth0 Community](https://community.auth0.com/).
+- For paid support plans, please [contact us directly](https://auth0.com/contact-us).
+- For more information about Auth0 Support, please visit our [Support Center](https://support.auth0.com/).
diff --git a/docs/Telescope.md b/docs/Telescope.md
new file mode 100644
index 00000000..2c51ea71
--- /dev/null
+++ b/docs/Telescope.md
@@ -0,0 +1,50 @@
+# Laravel Telescope
+
+As of 7.11.0, the Auth0 Laravel SDK is compatible with Laravel's Telescope debugging package. However, there are some caveats to be aware of when using the two together.
+
+## Cause of Potential Issues
+
+Issues stem from the fact that Telescope attempts to attribute events it's recording to the authenticated user. While this is useful information to log, it presents a problem. Because Telescope hooks into a number number of events (including the cache, queries, and events system) that the SDK raises during its authentication resolution process, this can cause an infinite loop.
+
+When a request to your application occurs, the SDK works to determine if the end user is authenticated. It executes a number of authenticated related events that Telescope happens to record by default. When these events are recorded by Telescope it asks the authentication API to determine if the end user is authenticated, which in turn calls the SDK to determine if the end user is authenticated, and thus the loop begins.
+
+7.11.0 introduced special checks for when Telescope is installed to prevent this from occurring, but it may not cover all cases.
+
+If you are encountering Telescope causing infinite loops, you may need to disable the offending watchers in your `config/telescope.php` file. Alternatively, you can try wrapping any problematic code in Telescope's `withoutRecording()` method to prevent it from being recorded by Telescope. For example:
+
+```php
+\Laravel\Telescope\Telescope::withoutRecording(function () {
+ // Your code here...
+});
+```
+
+## Missing Authentication Information from Telescope
+
+A side effect of the workarounds introduced in 7.11.0 that prevent Telescope from causing infinite loops is that Telescope may be unable to attribute recorded events triggered by the SDK to the authenticated user. This is intentional and necessary, and not a bug.
+
+## SDK <7.11.0 Workarounds
+
+In versions prior to 7.11.0, you may encounter a compatibility issue with the SDK and Telescope when installed and enabled together. You may need to disable offending watchers in your `config/telescope.php` file to resolve this.
+
+For example, if you are encountering issues with Telescope's `EventWatcher`, you can disable it in your `config/telescope.php` file, or ignore specific SDK events that are causing the issue. For example:
+
+```php
+ [
+ Watchers\EventWatcher::class => [
+ 'enabled' => env('TELESCOPE_EVENT_WATCHER', true),
+ 'ignore' => [
+ \Auth0\Laravel\Events\Configuration\BuiltConfigurationEvent::class,
+ \Auth0\Laravel\Events\Configuration\BuildingConfigurationEvent::class,
+ ],
+ ],
+ ],
+
+ // Other configuration options left out for brevity...
+];
+```
diff --git a/docs/Users.md b/docs/Users.md
new file mode 100644
index 00000000..b441e8f3
--- /dev/null
+++ b/docs/Users.md
@@ -0,0 +1,195 @@
+# Users
+
+- [User Persistenece](#user-persistenece)
+- [Best Practices](#best-practices)
+- [Retrieving User Information](#retrieving-user-information)
+- [Updating User Information](#updating-user-information)
+- [Extending the SDK](#extending-the-sdk)
+ - [User Repositories](#user-repositories)
+ - [Eloquent User Models](#eloquent-user-models)
+
+## User Persistence
+
+By default the SDK does not persist user information to a database.
+
+- When a user authenticates with your application, the SDK retrieves their profile data from Auth0 and stores it within their session.
+- During each subsequent request, the SDK retrieves the stored profile data from the session and constructs a model representing the authenticated user from it.
+- This user model is available to your application via the `Auth` facade or `auth()` helper for the duration of the current request.
+
+Later in this guide we'll demonstrate how you can extend this default behavior to persist that profile data to your application's database, if desired.
+
+## Best Practices
+
+Auth0 provides a number of features that can simplify your application's authentication and authorization workflows. It may be helpful to keep the following best practices in mind as you integrate the SDK into your application:
+
+- Treat Auth0 as the single source of truth about your users.
+- If you must store user information in a database, store as little as possible. Treat any stored data as a cache, and sync it regularly using [the Management API](./Management.md).
+- Always use the [the Management API](./Management.md) to update user information. If you're storing user information in a database, sync those changes to your database as needed, not the other way around.
+
+## Retrieving User Information
+
+To retrieve information about the currently authenticated user, use the `user()` method on the `Auth` facade or `auth()` helper.
+
+```php
+auth()->user();
+```
+
+You can also retrieve information on any user using [the Management API](./Management.md). This also returns extended information not usually contained in the session state, such as user metadata.
+
+```php
+use Auth0\Laravel\Facade\Auth0;
+
+Route::get('/profile', function () {
+ $profile = Auth0::management()->users()->get(auth()->id());
+ $profile = Auth0::json($profile);
+
+ $name = $profile['name'] ?? 'Unknown';
+ $email = $profile['email'] ?? 'Unknown';
+
+ return response("Hello {$name}! Your email address is {$email}.");
+})->middleware('auth');
+```
+
+## Updating User Information
+
+To update a user's information, use [the Management API](./Management.md).
+
+```php
+use Auth0\Laravel\Facade\Auth0;
+
+Route::get('/update', function () {
+ Auth0::management()
+ ->users()
+ ->update(
+ id: auth()->id(),
+ body: [
+ 'user_metadata' => [
+ 'last_visited' => time()
+ ]
+ ]
+ );
+})->middleware('auth');
+```
+
+## Extending the SDK
+
+### User Repositories
+
+By default the SDK does not store user information in your application's database. Instead, it uses the session to store the user's ID token, and retrieves user information from the token when needed. This is a good default behavior, but it may not be suitable for all applications.
+
+The SDK uses a repository pattern to allow you to customize how user information is stored and retrieved. This allows you to use your own database to cache user information between authentication requests, or to use a different storage mechanism entirely.
+
+#### Creating a User Repository
+
+You can create your own user repository by extending the SDK's `Auth0\Laravel\UserRepositoryAbstract` class implementing the `Auth0\Laravel\UserRepositoryContract` interface. Your repository class need only implement two public methods, both of which should accept a `user` array parameter.
+
+- `fromSession()` to construct a model for an authenticated user. When called, the `user` array will contain the decoded ID token for the authenticated user.
+- `fromAccessToken` to construct a model representing an access token request. When called, the `user` array will contain the decoded access token provided with the request.
+
+When these methods are called by the SDK, the `user` array will include all the information your application needs to construct an `Authenticatable` user model.
+
+The default `UserRepository` implementation looks like this:
+
+```php
+ "https://example.auth0.com/",
+ "aud" => "https://api.example.com/calendar/v1/",
+ "sub" => "auth0|123456",
+ "exp" => 1458872196,
+ "iat" => 1458785796,
+ "scope" => "read write",
+ ];
+ */
+
+ return User::where('auth0', $user['sub'])->first();
+ }
+
+ public function fromSession(array $user): ?Authenticatable
+ {
+ /*
+ $user = [ // Example of a decoded ID token
+ "iss" => "http://example.auth0.com",
+ "aud" => "client_id",
+ "sub" => "auth0|123456",
+ "exp" => 1458872196,
+ "iat" => 1458785796,
+ "name" => "Jane Doe",
+ "email" => "janedoe@example.com",
+ ];
+ */
+
+ $user = User::updateOrCreate(
+ attributes: [
+ 'auth0' => $user['sub'],
+ ],
+ values: [
+ 'name' => $user['name'] ?? '',
+ 'email' => $user['email'] ?? '',
+ 'email_verified' => $user['email_verified'] ?? false,
+ ]
+ );
+
+ return $user;
+ }
+}
+```
+
+Note that this example returns a custom user model, `App\Models\User`. You can find an example of this model in the [User Models](#user-models) section below.
+
+#### Registering a Repository
+
+You can override the SDK's default user repository by updating your application's `config/auth.php` file. Simply point the value of the `repository` key to your repository class.
+
+```php
+'providers' => [
+ 'auth0-provider' => [
+ 'driver' => 'auth0.provider',
+ 'repository' => \App\Repositories\UserRepository::class,
+ ],
+],
+```
+
+### Eloquent User Models
+
+Please see [Eloquent.md](./Eloquent.md) for guidance on using Eloquent models with the SDK.
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 54b1acc7..4b604cc9 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -1,20 +1,19 @@
includes:
- - vendor/phpstan/phpstan-strict-rules/rules.neon
- - vendor/nunomaduro/larastan/extension.neon
+ - ./vendor/phpstan/phpstan-strict-rules/rules.neon
+ - ./vendor/larastan/larastan/extension.neon
parameters:
level: max
paths:
- src
-
- bootstrapFiles:
- - tests/constants.php
+ - deprecated
ignoreErrors:
+ - '#Constructor of class (.*) has an unused parameter (.*).#'
- '#Method (.*) has parameter (.*) with no value type specified in iterable type array.#'
- - '#Cannot call method (.*) on mixed#'
- '#no value type specified in iterable type array.#'
- - '#Call to an undefined method Illuminate\\(.*).#'
reportUnmatchedIgnoredErrors: false
+ treatPhpDocTypesAsCertain: false
+ checkGenericClassInNonGenericObjectType: false
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index ba93708e..a52f9a82 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,9 +1,10 @@
-
+
-
- ./src/
-
+
+
+
+
@@ -11,8 +12,15 @@
-
-
-
+
+
diff --git a/pint.json b/pint.json
deleted file mode 100644
index 2bf69b74..00000000
--- a/pint.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "preset": "psr12",
- "exclude": ["tests"]
-}
diff --git a/psalm.xml.dist b/psalm.xml.dist
index e469154a..6e8412c3 100644
--- a/psalm.xml.dist
+++ b/psalm.xml.dist
@@ -5,22 +5,26 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
+ findUnusedBaselineEntry="true"
+ findUnusedCode="false"
+ allowStringToStandInForClass="true"
>
+
-
-
+
+
-
+
diff --git a/rector.php b/rector.php
index 4cbf85c5..0ce41546 100644
--- a/rector.php
+++ b/rector.php
@@ -2,21 +2,536 @@
declare(strict_types=1);
+use Rector\Arguments\Rector\ClassMethod\ArgumentAdderRector;
+use Rector\Arguments\Rector\FuncCall\FunctionArgumentDefaultValueReplacerRector;
+use Rector\Arguments\ValueObject\{ArgumentAdder,
+ ReplaceFuncCallArgumentDefaultValue};
+use Rector\CodeQuality\Rector\Array_\CallableThisArrayToAnonymousFunctionRector;
+use Rector\CodeQuality\Rector\Assign\{CombinedAssignRector,
+ SplitListAssignToSeparateLineRector};
+use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector;
+use Rector\CodeQuality\Rector\BooleanNot\{ReplaceMultipleBooleanNotRector,
+ SimplifyDeMorganBinaryRector};
+use Rector\CodeQuality\Rector\Catch_\ThrowWithPreviousExceptionRector;
+use Rector\CodeQuality\Rector\Class_\{CompleteDynamicPropertiesRector,
+ InlineConstructorDefaultToPropertyRector};
+use Rector\CodeQuality\Rector\ClassMethod\{InlineArrayReturnAssignRector,
+ NarrowUnionTypeDocRector,
+ OptionalParametersAfterRequiredRector,
+ ReturnTypeFromStrictScalarReturnExprRector};
+use Rector\CodeQuality\Rector\Concat\JoinStringConcatRector;
+use Rector\CodeQuality\Rector\Empty_\SimplifyEmptyCheckOnEmptyArrayRector;
+use Rector\CodeQuality\Rector\Equal\UseIdenticalOverEqualWithSameTypeRector;
+use Rector\CodeQuality\Rector\Expression\{InlineIfToExplicitIfRector,
+ TernaryFalseExpressionToIfRector};
+use Rector\CodeQuality\Rector\For_\{ForRepeatedCountToOwnVariableRector,
+ ForToForeachRector};
+use Rector\CodeQuality\Rector\Foreach_\{ForeachItemsAssignToEmptyArrayToAssignRector,
+ ForeachToInArrayRector,
+ SimplifyForeachToArrayFilterRector,
+ SimplifyForeachToCoalescingRector,
+ UnusedForeachValueToArrayKeysRector};
+use Rector\CodeQuality\Rector\FuncCall\{AddPregQuoteDelimiterRector,
+ ArrayKeysAndInArrayToArrayKeyExistsRector,
+ ArrayMergeOfNonArraysToSimpleArrayRector,
+ BoolvalToTypeCastRector,
+ CallUserFuncWithArrowFunctionToInlineRector,
+ ChangeArrayPushToArrayAssignRector,
+ CompactToVariablesRector,
+ FloatvalToTypeCastRector,
+ InlineIsAInstanceOfRector,
+ IntvalToTypeCastRector,
+ IsAWithStringWithThirdArgumentRector,
+ RemoveSoleValueSprintfRector,
+ SetTypeToCastRector,
+ SimplifyFuncGetArgsCountRector,
+ SimplifyInArrayValuesRector,
+ SimplifyRegexPatternRector,
+ SimplifyStrposLowerRector,
+ SingleInArrayToCompareRector,
+ StrvalToTypeCastRector,
+ UnwrapSprintfOneArgumentRector};
+use Rector\CodeQuality\Rector\FunctionLike\{RemoveAlwaysTrueConditionSetInConstructorRector,
+ SimplifyUselessLastVariableAssignRector,
+ SimplifyUselessVariableRector};
+use Rector\CodeQuality\Rector\Identical\{BooleanNotIdenticalToNotIdenticalRector,
+ FlipTypeControlToUseExclusiveTypeRector,
+ GetClassToInstanceOfRector,
+ SimplifyArraySearchRector,
+ SimplifyBoolIdenticalTrueRector,
+ SimplifyConditionsRector,
+ StrlenZeroToIdenticalEmptyStringRector};
+use Rector\CodeQuality\Rector\If_\{CombineIfRector,
+ ConsecutiveNullCompareReturnsToNullCoalesceQueueRector,
+ ExplicitBoolCompareRector,
+ ShortenElseIfRector,
+ SimplifyIfElseToTernaryRector,
+ SimplifyIfExactValueReturnValueRector,
+ SimplifyIfNotNullReturnRector,
+ SimplifyIfNullableReturnRector,
+ SimplifyIfReturnBoolRector};
+use Rector\CodeQuality\Rector\Include_\AbsolutizeRequireAndIncludePathRector;
+use Rector\CodeQuality\Rector\Isset_\IssetOnPropertyObjectToPropertyExistsRector;
+use Rector\CodeQuality\Rector\LogicalAnd\{AndAssignsToSeparateLinesRector,
+ LogicalToBooleanRector};
+use Rector\CodeQuality\Rector\New_\NewStaticToNewSelfRector;
+use Rector\CodeQuality\Rector\NotEqual\CommonNotEqualRector;
+use Rector\CodeQuality\Rector\PropertyFetch\ExplicitMethodCallOverMagicGetSetRector;
+use Rector\CodeQuality\Rector\Switch_\SingularSwitchToIfRector;
+use Rector\CodeQuality\Rector\Ternary\{ArrayKeyExistsTernaryThenValueToCoalescingRector,
+ SimplifyTautologyTernaryRector,
+ SwitchNegatedTernaryRector,
+ TernaryEmptyArrayArrayDimFetchToCoalesceRector,
+ UnnecessaryTernaryExpressionRector};
+use Rector\CodingStyle\Rector\ArrowFunction\StaticArrowFunctionRector;
+use Rector\CodingStyle\Rector\Assign\SplitDoubleAssignRector;
+use Rector\CodingStyle\Rector\Catch_\CatchExceptionNameMatchingTypeRector;
+use Rector\CodingStyle\Rector\Class_\AddArrayDefaultToArrayPropertyRector;
+use Rector\CodingStyle\Rector\ClassConst\{RemoveFinalFromConstRector, SplitGroupedClassConstantsRector, VarConstantCommentRector};
+use Rector\CodingStyle\Rector\ClassMethod\{FuncGetArgsToVariadicParamRector, MakeInheritedMethodVisibilitySameAsParentRector, NewlineBeforeNewAssignSetRector, RemoveDoubleUnderscoreInMethodNameRector, UnSpreadOperatorRector};
+use Rector\CodingStyle\Rector\Closure\StaticClosureRector;
+use Rector\CodingStyle\Rector\Encapsed\{EncapsedStringsToSprintfRector, WrapEncapsedVariableInCurlyBracesRector};
+use Rector\CodingStyle\Rector\FuncCall\{CallUserFuncArrayToVariadicRector, CallUserFuncToMethodCallRector, ConsistentImplodeRector, ConsistentPregDelimiterRector, CountArrayToEmptyArrayComparisonRector, StrictArraySearchRector, VersionCompareFuncCallToConstantRector};
+use Rector\CodingStyle\Rector\If_\NullableCompareToNullRector;
+use Rector\CodingStyle\Rector\Plus\UseIncrementAssignRector;
+use Rector\CodingStyle\Rector\PostInc\PostIncDecToPreIncDecRector;
+use Rector\CodingStyle\Rector\Property\{AddFalseDefaultToBoolPropertyRector, SplitGroupedPropertiesRector};
+use Rector\CodingStyle\Rector\String_\{SymplifyQuoteEscapeRector, UseClassKeywordForClassNameResolutionRector};
+use Rector\CodingStyle\Rector\Switch_\BinarySwitchToIfElseRector;
+use Rector\CodingStyle\Rector\Ternary\TernaryConditionVariableAssignmentRector;
+use Rector\CodingStyle\Rector\Use_\SeparateMultiUseImportsRector;
use Rector\Config\RectorConfig;
-use Rector\Core\ValueObject\PhpVersion;
-use Rector\Php74\Rector\Property\TypedPropertyRector;
-use Rector\Set\ValueObject\SetList;
+use Rector\DeadCode\Rector\Array_\RemoveDuplicatedArrayKeyRector;
+use Rector\DeadCode\Rector\Assign\{RemoveDoubleAssignRector,
+ RemoveUnusedVariableAssignRector};
+use Rector\DeadCode\Rector\BinaryOp\RemoveDuplicatedInstanceOfRector;
+use Rector\DeadCode\Rector\BooleanAnd\RemoveAndTrueRector;
+use Rector\DeadCode\Rector\ClassConst\RemoveUnusedPrivateClassConstantRector;
+use Rector\DeadCode\Rector\ClassMethod\{RemoveDelegatingParentCallRector,
+ RemoveEmptyClassMethodRector,
+ RemoveLastReturnRector,
+ RemoveUnusedConstructorParamRector,
+ RemoveUnusedPrivateMethodParameterRector,
+ RemoveUnusedPromotedPropertyRector,
+ RemoveUselessReturnTagRector};
+use Rector\DeadCode\Rector\Expression\{RemoveDeadStmtRector,
+ SimplifyMirrorAssignRector};
+use Rector\DeadCode\Rector\For_\{RemoveDeadContinueRector,
+ RemoveDeadIfForeachForRector,
+ RemoveDeadLoopRector};
+use Rector\DeadCode\Rector\Foreach_\RemoveUnusedForeachKeyRector;
+use Rector\DeadCode\Rector\FunctionLike\{RemoveDeadReturnRector,
+ RemoveDuplicatedIfReturnRector};
+use Rector\DeadCode\Rector\If_\{
+ RemoveUnusedNonEmptyArrayBeforeForeachRector,
+ SimplifyIfElseWithSameContentRector,
+ UnwrapFutureCompatibleIfPhpVersionRector};
+use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector;
+use Rector\DeadCode\Rector\Node\RemoveNonExistingVarAnnotationRector;
+use Rector\DeadCode\Rector\Plus\RemoveDeadZeroAndOneOperationRector;
+use Rector\DeadCode\Rector\Property\{RemoveUnusedPrivatePropertyRector,
+ RemoveUselessVarTagRector};
+use Rector\DeadCode\Rector\PropertyProperty\RemoveNullPropertyInitializationRector;
+use Rector\DeadCode\Rector\Return_\RemoveDeadConditionAboveReturnRector;
+use Rector\DeadCode\Rector\StaticCall\RemoveParentCallWithoutParentRector;
+use Rector\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector;
+use Rector\DeadCode\Rector\StmtsAwareInterface\{RemoveJustPropertyFetchForAssignRector,
+ RemoveJustVariableAssignRector};
+use Rector\DeadCode\Rector\Switch_\RemoveDuplicatedCaseInSwitchRector;
+use Rector\DeadCode\Rector\Ternary\TernaryToBooleanOrFalseToBooleanAndRector;
+use Rector\DeadCode\Rector\TryCatch\RemoveDeadTryCatchRector;
+use Rector\DependencyInjection\Rector\Class_\ActionInjectionToConstructorInjectionRector;
+use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector;
+use Rector\EarlyReturn\Rector\If_\{
+ ChangeIfElseValueAssignToEarlyReturnRector,
+ ChangeNestedIfsToEarlyReturnRector,
+ ChangeOrIfContinueToMultiContinueRector,
+ RemoveAlwaysElseRector};
+use Rector\EarlyReturn\Rector\Return_\{
+ ReturnBinaryAndToEarlyReturnRector,
+ ReturnBinaryOrToEarlyReturnRector};
+use Rector\EarlyReturn\Rector\StmtsAwareInterface\ReturnEarlyIfVariableRector;
-return static function (RectorConfig $rectorConfig): void {
- $rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/tests']);
+use Rector\Naming\Rector\Foreach_\{RenameForeachValueVariableToMatchExprVariableRector,
+ RenameForeachValueVariableToMatchMethodCallReturnTypeRector};
+use Rector\Php52\Rector\Property\VarToPublicPropertyRector;
+use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector;
+use Rector\Php80\Rector\Catch_\RemoveUnusedVariableInCatchRector;
+use Rector\Php80\Rector\Class_\{ClassPropertyAssignToConstructorPromotionRector,
+ StringableForToStringRector};
+use Rector\Php80\Rector\ClassConstFetch\ClassOnThisVariableObjectRector;
+use Rector\Php80\Rector\ClassMethod\{AddParamBasedOnParentClassMethodRector,
+ FinalPrivateToPrivateVisibilityRector,
+ SetStateToStaticRector};
+use Rector\Php80\Rector\FuncCall\{ClassOnObjectRector,
+ Php8ResourceReturnToObjectRector,
+ TokenGetAllToObjectRector};
+
+use Rector\Php80\Rector\Identical\{StrEndsWithRector,
+ StrStartsWithRector};
+use Rector\Php80\Rector\NotIdentical\StrContainsRector;
+use Rector\Php80\Rector\Switch_\ChangeSwitchToMatchRector;
+use Rector\Php80\Rector\Ternary\GetDebugTypeRector;
+use Rector\PHPUnit\Rector\ClassMethod\RemoveEmptyTestMethodRector;
+use Rector\Privatization\Rector\Class_\{ChangeGlobalVariablesToPropertiesRector,
+ ChangeReadOnlyVariableWithDefaultValueToConstantRector,
+ FinalizeClassesWithoutChildrenRector};
+use Rector\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector;
+use Rector\Privatization\Rector\Property\{ChangeReadOnlyPropertyWithDefaultValueToConstantRector,
+ PrivatizeFinalClassPropertyRector};
+use Rector\PSR4\Rector\FileWithoutNamespace\NormalizeNamespaceByPSR4ComposerAutoloadRector;
+use Rector\PSR4\Rector\Namespace_\MultipleClassFileToPsr4ClassesRector;
+use Rector\Renaming\Rector\FuncCall\RenameFunctionRector;
+use Rector\Transform\Rector\FuncCall\FuncCallToConstFetchRector;
+use Rector\Transform\Rector\StaticCall\StaticCallToFuncCallRector;
+use Rector\Transform\ValueObject\StaticCallToFuncCall;
+use Rector\TypeDeclaration\Rector\ArrowFunction\AddArrowFunctionReturnTypeRector;
+use Rector\TypeDeclaration\Rector\Class_\{PropertyTypeFromStrictSetterGetterRector,
+ ReturnTypeFromStrictTernaryRector};
+use Rector\TypeDeclaration\Rector\ClassMethod\{AddMethodCallBasedStrictParamTypeRector,
+ AddParamTypeBasedOnPHPUnitDataProviderRector,
+ AddReturnTypeDeclarationBasedOnParentClassMethodRector,
+ AddVoidReturnTypeWhereNoReturnRector,
+ ArrayShapeFromConstantArrayReturnRector,
+ ParamAnnotationIncorrectNullableRector,
+ ParamTypeByMethodCallTypeRector,
+ ParamTypeByParentCallTypeRector,
+ ReturnAnnotationIncorrectNullableRector,
+ ReturnNeverTypeRector,
+ ReturnTypeFromReturnDirectArrayRector,
+ ReturnTypeFromReturnNewRector,
+ ReturnTypeFromStrictBoolReturnExprRector,
+ ReturnTypeFromStrictConstantReturnRector,
+ ReturnTypeFromStrictNativeCallRector,
+ ReturnTypeFromStrictNewArrayRector,
+ ReturnTypeFromStrictTypedCallRector,
+ ReturnTypeFromStrictTypedPropertyRector};
+use Rector\TypeDeclaration\Rector\Closure\AddClosureReturnTypeRector;
+use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector;
+use Rector\TypeDeclaration\Rector\FunctionLike\{AddParamTypeSplFixedArrayRector,
+ AddReturnTypeDeclarationFromYieldsRector};
+use Rector\TypeDeclaration\Rector\Param\ParamTypeFromStrictTypedPropertyRector;
+use Rector\TypeDeclaration\Rector\Property\{TypedPropertyFromAssignsRector,
+ TypedPropertyFromStrictConstructorRector,
+ TypedPropertyFromStrictGetterMethodReturnTypeRector,
+ TypedPropertyFromStrictSetUpRector,
+ VarAnnotationIncorrectNullableRector};
- $rectorConfig->sets([
- SetList::CODE_QUALITY,
+return static function (RectorConfig $rectorConfig): void {
+ $rectorConfig->paths([
+ __DIR__ . '/config',
+ __DIR__ . '/src',
]);
- $rectorConfig->rule(TypedPropertyRector::class);
+ $rectorConfig->ruleWithConfiguration(
+ RenameFunctionRector::class,
+ [
+ 'chop' => 'rtrim',
+ 'doubleval' => 'floatval',
+ 'fputs' => 'fwrite',
+ 'gzputs' => 'gzwrites',
+ 'ini_alter' => 'ini_set',
+ 'is_double' => 'is_float',
+ 'is_integer' => 'is_int',
+ 'is_long' => 'is_int',
+ 'is_real' => 'is_float',
+ 'is_writeable' => 'is_writable',
+ 'join' => 'implode',
+ 'key_exists' => 'array_key_exists',
+ 'mbstrcut' => 'mb_strcut',
+ 'mbstrlen' => 'mb_strlen',
+ 'mbstrpos' => 'mb_strpos',
+ 'mbstrrpos' => 'mb_strrpos',
+ 'mbsubstr' => 'mb_substr',
+ 'pos' => 'current',
+ 'sizeof' => 'count',
+ 'split' => 'explode',
+ 'strchr' => 'strstr',
+ ],
+ );
+
+ $rectorConfig->ruleWithConfiguration(
+ StaticCallToFuncCallRector::class,
+ [
+ new StaticCallToFuncCall('Nette\\Utils\\Strings', 'contains', 'str_contains'),
+ new StaticCallToFuncCall('Nette\\Utils\\Strings', 'endsWith', 'str_ends_with'),
+ new StaticCallToFuncCall('Nette\\Utils\\Strings', 'startsWith', 'str_starts_with'),
+ ],
+ );
- $rectorConfig->phpVersion(PhpVersion::PHP_80);
+ $rectorConfig->ruleWithConfiguration(
+ ArgumentAdderRector::class,
+ [new ArgumentAdder('Nette\\Utils\\Strings', 'replace', 2, 'replacement', '')],
+ );
- $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist');
+ $rectorConfig->ruleWithConfiguration(
+ RenameFunctionRector::class,
+ [
+ 'pg_clientencoding' => 'pg_client_encoding',
+ 'pg_cmdtuples' => 'pg_affected_rows',
+ 'pg_errormessage' => 'pg_last_error',
+ 'pg_fieldisnull' => 'pg_field_is_null',
+ 'pg_fieldname' => 'pg_field_name',
+ 'pg_fieldnum' => 'pg_field_num',
+ 'pg_fieldprtlen' => 'pg_field_prtlen',
+ 'pg_fieldsize' => 'pg_field_size',
+ 'pg_fieldtype' => 'pg_field_type',
+ 'pg_freeresult' => 'pg_free_result',
+ 'pg_getlastoid' => 'pg_last_oid',
+ 'pg_loclose' => 'pg_lo_close',
+ 'pg_locreate' => 'pg_lo_create',
+ 'pg_loexport' => 'pg_lo_export',
+ 'pg_loimport' => 'pg_lo_import',
+ 'pg_loopen' => 'pg_lo_open',
+ 'pg_loread' => 'pg_lo_read',
+ 'pg_loreadall' => 'pg_lo_read_all',
+ 'pg_lounlink' => 'pg_lo_unlink',
+ 'pg_lowrite' => 'pg_lo_write',
+ 'pg_numfields' => 'pg_num_fields',
+ 'pg_numrows' => 'pg_num_rows',
+ 'pg_result' => 'pg_fetch_result',
+ 'pg_setclientencoding' => 'pg_set_client_encoding',
+ ],
+ );
+
+ $rectorConfig->ruleWithConfiguration(
+ FunctionArgumentDefaultValueReplacerRector::class,
+ [
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '', '!='),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '!', '!='),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'g', 'gt'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'l', 'lt'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'),
+ new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'n', 'ne'),
+ ],
+ );
+
+ $rectorConfig->ruleWithConfiguration(
+ FuncCallToConstFetchRector::class,
+ [
+ 'php_sapi_name' => 'PHP_SAPI',
+ 'pi' => 'M_PI',
+ ],
+ );
+
+ $rectorConfig->rules([
+ AbsolutizeRequireAndIncludePathRector::class,
+ // ActionInjectionToConstructorInjectionRector::class,
+ // AddArrayDefaultToArrayPropertyRector::class,
+ AddArrowFunctionReturnTypeRector::class,
+ // AddClosureReturnTypeRector::class,
+ // AddFalseDefaultToBoolPropertyRector::class,
+ AddMethodCallBasedStrictParamTypeRector::class,
+ AddParamBasedOnParentClassMethodRector::class,
+ AddParamTypeBasedOnPHPUnitDataProviderRector::class,
+ AddParamTypeSplFixedArrayRector::class,
+ // AddPregQuoteDelimiterRector::class,
+ AddReturnTypeDeclarationBasedOnParentClassMethodRector::class,
+ AddReturnTypeDeclarationFromYieldsRector::class,
+ AddVoidReturnTypeWhereNoReturnRector::class,
+ AndAssignsToSeparateLinesRector::class,
+ ArrayKeyExistsTernaryThenValueToCoalescingRector::class,
+ // ArrayKeysAndInArrayToArrayKeyExistsRector::class,
+ ArrayMergeOfNonArraysToSimpleArrayRector::class,
+ // ArrayShapeFromConstantArrayReturnRector::class,
+ // BinarySwitchToIfElseRector::class,
+ BooleanNotIdenticalToNotIdenticalRector::class,
+ BoolvalToTypeCastRector::class,
+ CallableThisArrayToAnonymousFunctionRector::class,
+ CallUserFuncArrayToVariadicRector::class,
+ CallUserFuncToMethodCallRector::class,
+ CallUserFuncWithArrowFunctionToInlineRector::class,
+ CatchExceptionNameMatchingTypeRector::class,
+ ChangeArrayPushToArrayAssignRector::class,
+ // ChangeGlobalVariablesToPropertiesRector::class,
+ ChangeIfElseValueAssignToEarlyReturnRector::class,
+ ChangeNestedForeachIfsToEarlyContinueRector::class,
+ ChangeNestedIfsToEarlyReturnRector::class,
+ ChangeOrIfContinueToMultiContinueRector::class,
+ // ChangeReadOnlyPropertyWithDefaultValueToConstantRector::class,
+ // ChangeReadOnlyVariableWithDefaultValueToConstantRector::class,
+ ChangeSwitchToMatchRector::class,
+ ClassOnObjectRector::class,
+ ClassOnThisVariableObjectRector::class,
+ ClassPropertyAssignToConstructorPromotionRector::class,
+ CombinedAssignRector::class,
+ CombineIfRector::class,
+ CommonNotEqualRector::class,
+ CompactToVariablesRector::class,
+ CompleteDynamicPropertiesRector::class,
+ ConsecutiveNullCompareReturnsToNullCoalesceQueueRector::class,
+ ConsistentImplodeRector::class,
+ // ConsistentPregDelimiterRector::class,
+ CountArrayToEmptyArrayComparisonRector::class,
+ EmptyOnNullableObjectToInstanceOfRector::class,
+ EncapsedStringsToSprintfRector::class,
+ ExplicitBoolCompareRector::class,
+ // ExplicitMethodCallOverMagicGetSetRector::class,
+ // FinalizeClassesWithoutChildrenRector::class,
+ FinalPrivateToPrivateVisibilityRector::class,
+ FlipTypeControlToUseExclusiveTypeRector::class,
+ FloatvalToTypeCastRector::class,
+ ForeachItemsAssignToEmptyArrayToAssignRector::class,
+ ForeachToInArrayRector::class,
+ ForRepeatedCountToOwnVariableRector::class,
+ // ForToForeachRector::class,
+ FuncGetArgsToVariadicParamRector::class,
+ GetClassToInstanceOfRector::class,
+ GetDebugTypeRector::class,
+ InlineArrayReturnAssignRector::class,
+ InlineConstructorDefaultToPropertyRector::class,
+ InlineIfToExplicitIfRector::class,
+ InlineIsAInstanceOfRector::class,
+ IntvalToTypeCastRector::class,
+ IsAWithStringWithThirdArgumentRector::class,
+ IssetOnPropertyObjectToPropertyExistsRector::class,
+ JoinStringConcatRector::class,
+ LogicalToBooleanRector::class,
+ MakeInheritedMethodVisibilitySameAsParentRector::class,
+ // MultipleClassFileToPsr4ClassesRector::class,
+ // NarrowUnionTypeDocRector::class,
+ NewlineBeforeNewAssignSetRector::class,
+ NewStaticToNewSelfRector::class,
+ // NormalizeNamespaceByPSR4ComposerAutoloadRector::class,
+ NullableCompareToNullRector::class,
+ OptionalParametersAfterRequiredRector::class,
+ // ParamAnnotationIncorrectNullableRector::class,
+ ParamTypeByMethodCallTypeRector::class,
+ ParamTypeByParentCallTypeRector::class,
+ // ParamTypeFromStrictTypedPropertyRector::class,
+ // Php8ResourceReturnToObjectRector::class,
+ PostIncDecToPreIncDecRector::class,
+ PrivatizeFinalClassMethodRector::class,
+ PrivatizeFinalClassPropertyRector::class,
+ PropertyTypeFromStrictSetterGetterRector::class,
+ RemoveAlwaysElseRector::class,
+ // RemoveAlwaysTrueConditionSetInConstructorRector::class,
+ RemoveAndTrueRector::class,
+ RemoveDeadConditionAboveReturnRector::class,
+ RemoveDeadContinueRector::class,
+ RemoveDeadIfForeachForRector::class,
+ RemoveDeadLoopRector::class,
+ RemoveDeadReturnRector::class,
+ RemoveDeadStmtRector::class,
+ RemoveDeadTryCatchRector::class,
+ RemoveDeadZeroAndOneOperationRector::class,
+ // RemoveDelegatingParentCallRector::class,
+ RemoveDoubleAssignRector::class,
+ // RemoveDoubleUnderscoreInMethodNameRector::class,
+ RemoveDuplicatedArrayKeyRector::class,
+ RemoveDuplicatedCaseInSwitchRector::class,
+ // RemoveDuplicatedIfReturnRector::class,
+ // RemoveDuplicatedInstanceOfRector::class,
+ RemoveEmptyClassMethodRector::class,
+ // RemoveEmptyMethodCallRector::class,
+ // RemoveEmptyTestMethodRector::class,
+ RemoveExtraParametersRector::class,
+ RemoveFinalFromConstRector::class,
+ // RemoveJustPropertyFetchForAssignRector::class,
+ // RemoveJustVariableAssignRector::class,
+ // RemoveLastReturnRector::class,
+ // RemoveNonExistingVarAnnotationRector::class,
+ RemoveNullPropertyInitializationRector::class,
+ RemoveParentCallWithoutParentRector::class,
+ RemoveSoleValueSprintfRector::class,
+ RemoveUnreachableStatementRector::class,
+ RemoveUnusedConstructorParamRector::class,
+ RemoveUnusedForeachKeyRector::class,
+ RemoveUnusedNonEmptyArrayBeforeForeachRector::class,
+ RemoveUnusedPrivateClassConstantRector::class,
+ RemoveUnusedPrivateMethodParameterRector::class,
+ RemoveUnusedPrivatePropertyRector::class,
+ RemoveUnusedPromotedPropertyRector::class,
+ RemoveUnusedVariableAssignRector::class,
+ RemoveUnusedVariableInCatchRector::class,
+ RemoveUselessReturnTagRector::class,
+ RemoveUselessVarTagRector::class,
+ RenameForeachValueVariableToMatchExprVariableRector::class,
+ RenameForeachValueVariableToMatchMethodCallReturnTypeRector::class,
+ ReplaceMultipleBooleanNotRector::class,
+ // ReturnAnnotationIncorrectNullableRector::class,
+ // ReturnBinaryAndToEarlyReturnRector::class,
+ ReturnBinaryOrToEarlyReturnRector::class,
+ ReturnEarlyIfVariableRector::class,
+ ReturnNeverTypeRector::class,
+ ReturnTypeFromReturnDirectArrayRector::class,
+ ReturnTypeFromReturnNewRector::class,
+ ReturnTypeFromStrictBoolReturnExprRector::class,
+ ReturnTypeFromStrictConstantReturnRector::class,
+ ReturnTypeFromStrictNativeCallRector::class,
+ ReturnTypeFromStrictNewArrayRector::class,
+ // ReturnTypeFromStrictScalarReturnExprRector::class,
+ ReturnTypeFromStrictTernaryRector::class,
+ ReturnTypeFromStrictTypedCallRector::class,
+ ReturnTypeFromStrictTypedPropertyRector::class,
+ SeparateMultiUseImportsRector::class,
+ SetStateToStaticRector::class,
+ SetTypeToCastRector::class,
+ ShortenElseIfRector::class,
+ SimplifyArraySearchRector::class,
+ SimplifyBoolIdenticalTrueRector::class,
+ SimplifyConditionsRector::class,
+ SimplifyDeMorganBinaryRector::class,
+ SimplifyEmptyArrayCheckRector::class,
+ SimplifyEmptyCheckOnEmptyArrayRector::class,
+ // SimplifyForeachToArrayFilterRector::class,
+ SimplifyForeachToCoalescingRector::class,
+ SimplifyFuncGetArgsCountRector::class,
+ SimplifyIfElseToTernaryRector::class,
+ SimplifyIfElseWithSameContentRector::class,
+ // SimplifyIfExactValueReturnValueRector::class,
+ SimplifyIfNotNullReturnRector::class,
+ SimplifyIfNullableReturnRector::class,
+ SimplifyIfReturnBoolRector::class,
+ SimplifyInArrayValuesRector::class,
+ SimplifyMirrorAssignRector::class,
+ SimplifyRegexPatternRector::class,
+ SimplifyStrposLowerRector::class,
+ SimplifyTautologyTernaryRector::class,
+ // SimplifyUselessLastVariableAssignRector::class,
+ SimplifyUselessVariableRector::class,
+ SingleInArrayToCompareRector::class,
+ SingularSwitchToIfRector::class,
+ SplitDoubleAssignRector::class,
+ SplitGroupedClassConstantsRector::class,
+ SplitGroupedPropertiesRector::class,
+ // SplitListAssignToSeparateLineRector::class,
+ StaticArrowFunctionRector::class,
+ StaticClosureRector::class,
+ StrContainsRector::class,
+ StrEndsWithRector::class,
+ StrictArraySearchRector::class,
+ StringableForToStringRector::class,
+ StrlenZeroToIdenticalEmptyStringRector::class,
+ StrStartsWithRector::class,
+ StrvalToTypeCastRector::class,
+ SwitchNegatedTernaryRector::class,
+ SymplifyQuoteEscapeRector::class,
+ TernaryConditionVariableAssignmentRector::class,
+ TernaryEmptyArrayArrayDimFetchToCoalesceRector::class,
+ TernaryFalseExpressionToIfRector::class,
+ TernaryToBooleanOrFalseToBooleanAndRector::class,
+ ThrowWithPreviousExceptionRector::class,
+ // TokenGetAllToObjectRector::class,
+ TypedPropertyFromAssignsRector::class,
+ TypedPropertyFromStrictConstructorRector::class,
+ // TypedPropertyFromStrictGetterMethodReturnTypeRector::class,
+ TypedPropertyFromStrictSetUpRector::class,
+ UnnecessaryTernaryExpressionRector::class,
+ // UnSpreadOperatorRector::class,
+ UnusedForeachValueToArrayKeysRector::class,
+ UnwrapFutureCompatibleIfPhpVersionRector::class,
+ UnwrapSprintfOneArgumentRector::class,
+ UseClassKeywordForClassNameResolutionRector::class,
+ UseIdenticalOverEqualWithSameTypeRector::class,
+ UseIncrementAssignRector::class,
+ // VarAnnotationIncorrectNullableRector::class,
+ // VarConstantCommentRector::class,
+ VarToPublicPropertyRector::class,
+ VersionCompareFuncCallToConstantRector::class,
+ WrapEncapsedVariableInCurlyBracesRector::class,
+ ]);
};
diff --git a/src/Auth/Guard.php b/src/Auth/Guard.php
index 0e034961..8469ee1d 100644
--- a/src/Auth/Guard.php
+++ b/src/Auth/Guard.php
@@ -4,338 +4,240 @@
namespace Auth0\Laravel\Auth;
-use Auth0\Laravel\Auth0;
-use Auth0\Laravel\Contract\Auth\User\Provider;
-use Auth0\Laravel\Contract\StateInstance;
-use Auth0\Laravel\StateInstance as ConcreteStateInstance;
-use Auth0\SDK\Configuration\SdkConfiguration;
+use Auth0\Laravel\Entities\CredentialEntityContract;
+use Auth0\Laravel\Guards\{AuthenticationGuard, AuthenticationGuardContract, AuthorizationGuard, AuthorizationGuardContract, GuardAbstract, GuardContract};
use Illuminate\Contracts\Auth\Authenticatable;
-use Illuminate\Contracts\Auth\UserProvider;
-use Illuminate\Support\Facades\Session;
-use RuntimeException;
-final class Guard implements \Auth0\Laravel\Contract\Auth\Guard, \Illuminate\Contracts\Auth\Guard
+/**
+ * @deprecated 7.8.0 Please migrate to using either Auth0\Laravel\Guards\AuthenticationGuard or Auth0\Laravel\Guards\AuthorizationGuard.
+ *
+ * @api
+ */
+final class Guard extends GuardAbstract implements GuardContract
{
- /**
- * {@inheritdoc}
- */
- public function login(Authenticatable $user): self
- {
- $this->getState()->setUser($user);
+ private ?AuthenticationGuardContract $authenticator = null;
- return $this;
- }
+ private ?AuthorizationGuardContract $authorizer = null;
+
+ private ?int $credentialSource = null;
/**
- * {@inheritdoc}
+ * @param null|int $source Credential source in which to search. Defaults to searching all sources.
*/
- public function logout(): self
- {
- // Although user() should never return null in this instance, default to an empty dummy user in such an event to avoid throwing an exception.
- $user = $this->user() ?? new \Auth0\Laravel\Model\Stateful\User([]);
+ public function find(
+ ?int $source = null,
+ ): ?CredentialEntityContract {
+ $token = null;
+ $session = null;
- event(new \Illuminate\Auth\Events\Logout(Guard::class, $user));
+ if ($this->isImpersonating()) {
+ return $this->getImposter();
+ }
- app()->instance(StateInstance::class, null);
- Session::flush();
+ if (null === $source || self::SOURCE_TOKEN === $source) {
+ $token = $this->getAuthorizationGuard()->findToken();
+ }
- app(Auth0::class)->getSdk()->clear();
+ if (null === $source && ! $token instanceof CredentialEntityContract || self::SOURCE_SESSION === $source) {
+ $session = $this->getAuthenticationGuard()->findSession();
+ }
- return $this;
+ return $token ?? $session ?? null;
}
- /**
- * {@inheritdoc}
- */
- public function check(): bool
+ public function forgetUser(): self
{
- return null !== $this->user();
- }
+ $this->setCredential();
- /**
- * {@inheritdoc}
- */
- public function guest(): bool
- {
- return ! $this->check();
+ return $this;
}
- /**
- * {@inheritdoc}
- */
- public function user(): ?Authenticatable
+ public function getCredential(): ?CredentialEntityContract
{
- $user = $this->getState()->getUser();
-
- if (! $user instanceof Authenticatable) {
- $configuration = app(Auth0::class)->getConfiguration();
+ if ($this->isImpersonating()) {
+ return $this->getImposter();
+ }
- $apiOnly = \in_array($configuration->getStrategy(), [SdkConfiguration::STRATEGY_API, SdkConfiguration::STRATEGY_MANAGEMENT_API], true);
+ $token = null;
+ $session = null;
+ $source = $this->getCredentialSource();
- if ($apiOnly) {
- $user = $this->getUserFromToken();
- }
+ if (null === $source || self::SOURCE_TOKEN === $source) {
+ $token = $this->getAuthorizationGuard()->getCredential();
+ }
- if (! $apiOnly) {
- $user = $this->getUserFromSession();
- }
+ if (null === $source && ! $token instanceof CredentialEntityContract || self::SOURCE_SESSION === $source) {
+ $session = $this->getAuthenticationGuard()->getCredential();
}
- return $user;
+ return $token ?? $session ?? null;
}
/**
- * {@inheritdoc}
+ * Sets the currently authenticated user for the guard.
+ *
+ * @param null|CredentialEntityContract $credential Optional. The credential to use.
+ * @param null|int $source Optional. The source context in which to assign the user. Defaults to all sources.
*/
- public function id()
- {
- $response = null;
- $user = $this->user();
+ public function login(
+ ?CredentialEntityContract $credential,
+ ?int $source = null,
+ ): GuardContract {
+ $this->stopImpersonating();
- if (null !== $user) {
- $id = $user->getAuthIdentifier();
+ if (null === $source || self::SOURCE_TOKEN === $source) {
+ $this->getAuthorizationGuard()->login($credential);
+ }
- if (\is_string($id) || \is_int($id)) {
- $response = $id;
- }
+ if (null === $source || self::SOURCE_SESSION === $source) {
+ $this->getAuthenticationGuard()->login($credential);
}
- return $response;
- }
+ $this->credentialSource = $source;
- /**
- * {@inheritdoc}
- *
- * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
- */
- public function validate(array $credentials = []): bool
- {
- return false;
+ return $this;
}
- /**
- * {@inheritdoc}
- *
- * @psalm-suppress UnusedVariable
- */
- public function setUser(Authenticatable $user): self
+ public function logout(): GuardContract
{
- $user = $this->getState()->
- setUser($user);
+ if ($this->isImpersonating()) {
+ $this->stopImpersonating();
- return $this;
- }
+ return $this;
+ }
- /**
- * {@inheritdoc}
- */
- public function hasUser(): bool
- {
- return null !== $this->getState()->getUser();
- }
+ $source = $this->getCredentialSource();
- /**
- * {@inheritdoc}
- */
- public function hasScope(string $scope): bool
- {
- $state = $this->getState();
+ if (null === $source || self::SOURCE_TOKEN === $source) {
+ $this->getAuthorizationGuard()->logout();
+ }
- return \in_array($scope, $state->getAccessTokenScope() ?? [], true);
- }
+ if (null === $source || self::SOURCE_SESSION === $source) {
+ $this->getAuthenticationGuard()->logout();
+ }
- /**
- * Always returns false to keep third-party apps happy.
- */
- public function viaRemember(): bool
- {
- return false;
+ return $this;
}
- /**
- * Get the user context from a provided access token.
- */
- private function getUserFromToken(): ?Authenticatable
+ public function refreshUser(): void
{
- // Retrieve an available bearer token from the request.
- $request = request();
-
- // @phpstan-ignore-next-line
- if (! $request instanceof \Illuminate\Http\Request) {
- return null;
+ if ($this->isImpersonating()) {
+ return;
}
- $token = $request->bearerToken();
+ $source = $this->getCredentialSource();
- // If a session is not available, return null.
- if (! \is_string($token)) {
- return null;
+ if (null === $source || self::SOURCE_TOKEN === $source) {
+ $this->getAuthorizationGuard()->refreshUser();
}
- $event = new \Auth0\Laravel\Event\Stateless\TokenVerificationAttempting($token);
- event($event);
- $token = $event->getToken();
-
- try {
- // Attempt to decode the bearer token.
- $decoded = app(Auth0::class)->getSdk()->decode(
- token: $token,
- tokenType: \Auth0\SDK\Token::TYPE_TOKEN,
- )->toArray();
- } catch (\Auth0\SDK\Exception\InvalidTokenException $invalidToken) {
- event(new \Auth0\Laravel\Event\Stateless\TokenVerificationFailed($token, $invalidToken));
-
- // Invalid bearer token.
- return null;
+ if (null === $source || self::SOURCE_SESSION === $source) {
+ $this->getAuthenticationGuard()->refreshUser();
}
+ }
- // Query the UserProvider to retrieve tue user for the token.
- $provider = $this->getProvider();
-
- /**
- * @var Provider $provider
- * @var array{scope: string|null, exp: int|null} $decoded
- */
- $user = $provider->
- getRepository()->
- fromAccessToken($decoded);
-
- // Was a user retrieved successfully?
- if (null !== $user) {
- if (! $user instanceof \Auth0\Laravel\Contract\Model\Stateless\User) {
- exit('User model returned fromAccessToken must implement \Auth0\Laravel\Contract\Model\Stateless\User.');
- }
+ public function setCredential(
+ ?CredentialEntityContract $credential = null,
+ ?int $source = null,
+ ): GuardContract {
+ $this->stopImpersonating();
- event(new \Auth0\Laravel\Event\Stateless\TokenVerificationSucceeded($token, $decoded));
+ if (null === $source || self::SOURCE_TOKEN === $source) {
+ $this->getAuthorizationGuard()->setCredential($credential);
+ }
- $this->getState()->
- clear()->
- setDecoded($decoded)->
- setAccessToken($token)->
- setAccessTokenScope(explode(' ', $decoded['scope'] ?? ''))->
- setAccessTokenExpiration($decoded['exp'] ?? null);
+ if (null === $source || self::SOURCE_SESSION === $source) {
+ $this->getAuthenticationGuard()->setCredential($credential);
}
- return $user;
+ $this->credentialSource = $source;
+
+ return $this;
}
/**
- * Get the user context from an Auth0-PHP SDK session..
+ * @param CredentialEntityContract $credential
+ * @param ?int $source
*/
- private function getUserFromSession(): ?Authenticatable
- {
- // Retrieve an available session from the Auth0-PHP SDK.
- $session = app(Auth0::class)->getSdk()->getCredentials();
+ public function setImpersonating(
+ CredentialEntityContract $credential,
+ ?int $source = null,
+ ): self {
+ $this->impersonationSource = $source;
+ $this->impersonating = $credential;
- // If a session is not available, return null.
- if (null === $session) {
- return null;
- }
+ return $this;
+ }
- // Query the UserProvider to retrieve tue user for the token.
- $provider = $this->getProvider();
+ public function setUser(
+ Authenticatable $user,
+ ): self {
+ if ($this->isImpersonating()) {
+ if ($this->getImposter()?->getUser() === $user) {
+ return $this;
+ }
- /**
- * @var Provider $provider
- */
+ $this->stopImpersonating();
+ }
- // Query the UserProvider to retrieve tue user for the session.
- $user = $provider->
- getRepository()->
- fromSession($session->user); // @phpstan-ignore-line
+ $source = $this->getCredentialSource();
- // Was a user retrieved successfully?
- if (null !== $user) {
- if (! $user instanceof \Auth0\Laravel\Contract\Model\Stateful\User) {
- exit('User model returned fromSession must implement \Auth0\Laravel\Contract\Model\Stateful\User.');
- }
+ if (null === $source || self::SOURCE_TOKEN === $source) {
+ $this->getAuthorizationGuard()->setUser($user);
+ }
- $this->getState()->
- clear()->
- setDecoded($session->user)-> // @phpstan-ignore-line
- setIdToken($session->idToken)-> // @phpstan-ignore-line
- setAccessToken($session->accessToken)-> // @phpstan-ignore-line
- setAccessTokenScope($session->accessTokenScope)-> // @phpstan-ignore-line
- setAccessTokenExpiration($session->accessTokenExpiration)-> // @phpstan-ignore-line
- setRefreshToken($session->refreshToken); /** @phpstan-ignore-line */
- $user = $this->handleSessionExpiration($user);
+ if (null === $source || self::SOURCE_SESSION === $source) {
+ $this->getAuthenticationGuard()->setUser($user);
}
- return $user;
+ return $this;
}
- /**
- * Handle instances of session token expiration.
- */
- private function handleSessionExpiration(
- ?Authenticatable $user,
- ): ?Authenticatable {
- $state = $this->getState();
-
- // Unless our token expired, we have nothing to do here.
- if (true !== $state->getAccessTokenExpired()) {
- return $user;
+ public function user(): ?Authenticatable
+ {
+ if ($this->isImpersonating()) {
+ return $this->getImposter()?->getUser();
}
- // Do we have a refresh token?
- if (null !== $state->getRefreshToken()) {
- try {
- // Try to renew our token.
- app(Auth0::class)->getSdk()->renew();
- } catch (\Auth0\SDK\Exception\StateException $tokenRefreshFailed) {
- // Renew failed. Inform application.
- event(new \Auth0\Laravel\Event\Stateful\TokenRefreshFailed());
- }
+ $credential = $this->getCredential();
- // Retrieve updated state data
- $refreshed = app(Auth0::class)->getSdk()->getCredentials();
+ if ($credential instanceof CredentialEntityContract) {
+ return $credential->getUser();
+ }
- // @phpstan-ignore-next-line
- if (null !== $refreshed && false === $refreshed->accessTokenExpired) {
- event(new \Auth0\Laravel\Event\Stateful\TokenRefreshSucceeded());
+ // $source = $this->getCredentialSource();
+ // $token = null;
+ // $session = null;
- return $user;
- }
- }
+ // if (null === $source || self::SOURCE_TOKEN === $source) {
+ // $token = $this->getAuthorizationGuard()->user();
+ // }
- // We didn't have a refresh token, or the refresh failed.
- // Clear session.
- $state->clear();
- app(Auth0::class)->getSdk()->clear();
+ // if (null === $source || self::SOURCE_SESSION === $source) {
+ // $session = $this->getAuthenticationGuard()->user();
+ // }
- // Inform host application.
- event(new \Auth0\Laravel\Event\Stateful\TokenExpired());
+ // return $token ?? $session ?? null;
return null;
}
- /**
- * Return the current request's StateInstance singleton.
- */
- private function getState(): StateInstance
+ private function getAuthenticationGuard(): AuthenticationGuardContract
{
- return app(ConcreteStateInstance::class);
+ $this->sdk();
+
+ return $this->authenticator ??= new AuthenticationGuard(name: $this->name, config: $this->config, sdk: $this->sdk);
}
- /**
- * Return the current request's StateInstance singleton.
- */
- private function getProvider(): UserProvider
+ private function getAuthorizationGuard(): AuthorizationGuardContract
{
- static $provider = null;
+ $this->sdk();
- if (null === $provider) {
- /**
- * @var string|null $configured
- */
- $configured = config('auth.guards.auth0.provider') ?? \Auth0\Laravel\Auth\User\Provider::class;
- $provider = app('auth')->createUserProvider($configured);
-
- if (! $provider instanceof UserProvider) {
- throw new RuntimeException('Auth0: Unable to invoke UserProvider from application configuration.');
- }
- }
+ return $this->authorizer ??= new AuthorizationGuard(name: $this->name, config: $this->config, sdk: $this->sdk);
+ }
- return $provider;
+ private function getCredentialSource(): ?int
+ {
+ return $this->credentialSource;
}
}
diff --git a/src/Auth/User/Provider.php b/src/Auth/User/Provider.php
deleted file mode 100644
index 487fd927..00000000
--- a/src/Auth/User/Provider.php
+++ /dev/null
@@ -1,79 +0,0 @@
-getConfiguration());
- }
-
- $this->setSdkTelemetry();
-
- return self::$sdk;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setSdk(SDK $sdk): self
- {
- self::$sdk = $sdk;
- $this->setSdkTelemetry();
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getConfiguration(): Configuration
- {
- if (null === self::$configuration) {
- $config = config('auth0');
-
- /**
- * @var array $config
- */
- if (! isset($config['tokenCache']) || ! isset($config['managementTokenCache'])) {
- $cache = new LaravelCachePool();
-
- if (! isset($config['tokenCache'])) {
- $config['tokenCache'] = $cache;
- }
-
- if (! isset($config['managementTokenCache'])) {
- $config['managementTokenCache'] = $cache;
- }
- }
-
- $configuration = new Configuration($config);
-
- if (! \in_array($configuration->getStrategy(), [Configuration::STRATEGY_API, Configuration::STRATEGY_MANAGEMENT_API], true)) {
- // If no sessionStorage is defined, use an LaravelSession store instance.
- if (! isset($config['sessionStorage'])) {
- $configuration->setSessionStorage(
- sessionStorage: new LaravelSession(
- prefix: $configuration->getSessionStorageId(),
- ),
- );
- }
-
- // If no transientStorage is defined, use an LaravelSession store instance.
- if (! isset($config['transientStorage'])) {
- $configuration->setTransientStorage(
- transientStorage: new LaravelSession(
- prefix: $configuration->getTransientStorageId(),
- ),
- );
- }
- }
-
- // Give apps an opportunity to mutate the configuration before applying it.
- $event = new \Auth0\Laravel\Event\Configuration\Built($configuration);
- event($event);
-
- self::$configuration = $event->getConfiguration();
- }
-
- return self::$configuration;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setConfiguration(Configuration $configuration): self
- {
- self::$configuration = $configuration;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getState(): Contract\StateInstance
- {
- return app(StateInstance::class);
- }
-
- /**
- * Updates the Auth0 PHP SDK's telemetry to include the correct Laravel markers.
- */
- private function setSdkTelemetry(): self
- {
- \Auth0\SDK\Utility\HttpTelemetry::setEnvProperty('Laravel', app()->version());
- \Auth0\SDK\Utility\HttpTelemetry::setPackage('laravel-auth0', self::VERSION);
-
- return $this;
- }
+ use InstanceEntityTrait;
}
diff --git a/src/Bridges/BridgeAbstract.php b/src/Bridges/BridgeAbstract.php
new file mode 100644
index 00000000..d351f4ba
--- /dev/null
+++ b/src/Bridges/BridgeAbstract.php
@@ -0,0 +1,14 @@
+
+ */
+ protected array $deferred = [];
+
+ final public function clear(): bool
+ {
+ $this->deferred = [];
+
+ return $this->getCache()->flush();
+ }
+
+ final public function commit(): bool
+ {
+ $success = true;
+
+ foreach (array_keys($this->deferred) as $singleDeferred) {
+ $item = $this->getDeferred((string) $singleDeferred);
+
+ // @codeCoverageIgnoreStart
+ if ($item instanceof CacheItemInterface && ! $this->save($item)) {
+ $success = false;
+ }
+ // @codeCoverageIgnoreEnd
+ }
+
+ $this->deferred = [];
+
+ return $success;
+ }
+
+ /**
+ * @param string $key the key for which to return the corresponding Cache Item
+ */
+ final public function deleteItem(string $key): bool
+ {
+ return $this->getCache()->forget($key);
+ }
+
+ final public function deleteItems(array $keys): bool
+ {
+ $deleted = true;
+
+ foreach ($keys as $key) {
+ if (! $this->deleteItem($key)) {
+ $deleted = false;
+ }
+ }
+
+ return $deleted;
+ }
+
+ final public function getItem(string $key): CacheItemInterface
+ {
+ $value = $this->getCache()->get($key);
+
+ if (false === $value) {
+ return CacheItemBridge::miss($key);
+ }
+
+ return $this->createItem($key, $value);
+ }
+
+ /**
+ * @param string[] $keys
+ *
+ * @return CacheItemInterface[]
+ */
+ final public function getItems(array $keys = []): iterable
+ {
+ if ([] === $keys) {
+ return [];
+ }
+
+ $results = $this->getCache()->many($keys);
+ $items = [];
+
+ foreach ($results as $key => $value) {
+ $key = (string) $key;
+ $items[$key] = $this->createItem($key, $value);
+ }
+
+ return $items;
+ }
+
+ /**
+ * @param string $key the key for which to return the corresponding Cache Item
+ */
+ final public function hasItem(string $key): bool
+ {
+ return $this->getItem($key)
+ ->isHit();
+ }
+
+ final public function save(CacheItemInterface $item): bool
+ {
+ if (! $item instanceof CacheItemBridge) {
+ return false;
+ }
+
+ $value = serialize($item->getRawValue());
+ $key = $item->getKey();
+ $expires = $item->getExpiration();
+
+ if ($expires->getTimestamp() <= time()) {
+ return $this->deleteItem($key);
+ }
+
+ $ttl = $expires->getTimestamp() - time();
+
+ return $this->getCache()->put($key, $value, $ttl);
+ }
+
+ final public function saveDeferred(CacheItemInterface $item): bool
+ {
+ if (! $item instanceof CacheItemBridge) {
+ return false;
+ }
+
+ $this->deferred[$item->getKey()] = [
+ 'item' => $item,
+ 'expiration' => $item->getExpiration(),
+ ];
+
+ return true;
+ }
+
+ protected function createItem(string $key, mixed $value): CacheItemInterface
+ {
+ if (! is_string($value)) {
+ return CacheItemBridge::miss($key);
+ }
+
+ $value = unserialize($value);
+
+ if (false === $value) {
+ return CacheItemBridge::miss($key);
+ }
+
+ return new CacheItemBridge($key, $value, true);
+ }
+
+ protected function getCache(): Store
+ {
+ $cache = cache();
+
+ // @codeCoverageIgnoreStart
+ if (! $cache instanceof CacheManager) {
+ throw new RuntimeException('Cache store is not an instance of Illuminate\Contracts\Cache\CacheManager');
+ }
+ // @codeCoverageIgnoreEnd
+
+ return $cache->getStore();
+ }
+
+ /**
+ * @param string $key the key for which to return the corresponding Cache Item
+ *
+ * @codeCoverageIgnore
+ */
+ protected function getDeferred(string $key): ?CacheItemInterface
+ {
+ if (! isset($this->deferred[$key])) {
+ return null;
+ }
+
+ $deferred = $this->deferred[$key];
+ $item = clone $deferred['item'];
+ $expires = $deferred['expiration'];
+
+ if ($expires instanceof DateTimeInterface) {
+ $expires = $expires->getTimestamp();
+ }
+
+ if (null !== $expires && $expires <= time()) {
+ unset($this->deferred[$key]);
+
+ return null;
+ }
+
+ return $item;
+ }
+}
diff --git a/src/Bridges/CacheBridgeContract.php b/src/Bridges/CacheBridgeContract.php
new file mode 100644
index 00000000..5b058a9f
--- /dev/null
+++ b/src/Bridges/CacheBridgeContract.php
@@ -0,0 +1,14 @@
+expiration = match (true) {
+ null === $time => new DateTimeImmutable('now +1 year'),
+ is_int($time) => new DateTimeImmutable('now +' . (string) $time . ' seconds'),
+ $time instanceof DateInterval => (new DateTimeImmutable())->add($time),
+ };
+
+ return $this;
+ }
+
+ public function expiresAt(?DateTimeInterface $expiration): static
+ {
+ $this->expiration = $expiration ?? new DateTimeImmutable('now +1 year');
+
+ return $this;
+ }
+
+ public function set(mixed $value): static
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+
+ public static function miss(string $key): self
+ {
+ return new self(
+ key: $key,
+ value: null,
+ hit: false,
+ );
+ }
+}
diff --git a/src/Bridges/CacheItemBridgeAbstract.php b/src/Bridges/CacheItemBridgeAbstract.php
new file mode 100644
index 00000000..281fab0e
--- /dev/null
+++ b/src/Bridges/CacheItemBridgeAbstract.php
@@ -0,0 +1,60 @@
+isHit() ? $this->value : null;
+ }
+
+ /**
+ * Returns the expiration timestamp.
+ */
+ final public function getExpiration(): DateTimeInterface
+ {
+ return $this->expiration ?? new DateTimeImmutable('now +1 year');
+ }
+
+ final public function getKey(): string
+ {
+ return $this->key;
+ }
+
+ /**
+ * Returns the raw value, regardless of hit status.
+ */
+ final public function getRawValue(): mixed
+ {
+ return $this->value;
+ }
+
+ final public function isHit(): bool
+ {
+ return $this->hit;
+ }
+
+ abstract public function expiresAfter(int | DateInterval | null $time): static;
+
+ abstract public function expiresAt(?DateTimeInterface $expiration): static;
+
+ abstract public function set(mixed $value): static;
+}
diff --git a/src/Bridges/CacheItemBridgeContract.php b/src/Bridges/CacheItemBridgeContract.php
new file mode 100644
index 00000000..d2f26577
--- /dev/null
+++ b/src/Bridges/CacheItemBridgeContract.php
@@ -0,0 +1,20 @@
+setPrefix($prefix);
+ }
+
+ /**
+ * This method is required by the interface but is not used by this SDK.
+ *
+ * @param bool $deferring whether to defer persisting the storage state
+ */
+ final public function defer(bool $deferring): void
+ {
+ }
+
+ /**
+ * Delete a value from the Laravel session by key. (Key will be automatically prefixed with the SDK's configured namespace.).
+ *
+ * @param string $key session key to delete
+ *
+ * @throws SessionException If a Laravel session store is not available.
+ */
+ final public function delete(string $key): void
+ {
+ $payload = $this->getPayload() ?? [];
+
+ if (array_key_exists($key, $payload)) {
+ unset($payload[$key]);
+ $this->getStore()->put($this->getPrefix(), json_encode(array_filter($payload), JSON_THROW_ON_ERROR));
+ }
+ }
+
+ /**
+ * Retrieve a value from the Laravel session by key. (Key will be automatically prefixed with the SDK's configured namespace.).
+ *
+ * @param string $key session key to query
+ * @param mixed $default default to return if nothing was found
+ *
+ * @throws SessionException If a Laravel session store is not available.
+ */
+ final public function get(string $key, $default = null): mixed
+ {
+ $payload = $this->getPayload() ?? [];
+
+ return $payload[$key] ?? $default;
+ }
+
+ /**
+ * Get all values from the Laravel session that are prefixed with the SDK's configured namespace.
+ *
+ * @throws SessionException If a Laravel session store is not available.
+ */
+ final public function getAll(): array
+ {
+ return $this->getPayload() ?? [];
+ }
+
+ /**
+ * Get the prefix used for all session keys.
+ *
+ * @return string Prefix used for all session keys.
+ */
+ final public function getPrefix(): string
+ {
+ return $this->prefix;
+ }
+
+ /**
+ * Delete all values from the Laravel session that are prefixed with the SDK's configured namespace.
+ *
+ * @throws SessionException If a Laravel session store is not available.
+ */
+ final public function purge(): void
+ {
+ $this->getStore()->forget($this->getPrefix());
+ }
+
+ /**
+ * Store a value in the Laravel session. (Key will be automatically prefixed with the SDK's configured namespace.).
+ *
+ * @param string $key session key to set
+ * @param mixed $value value to use
+ *
+ * @throws SessionException If a Laravel session store is not available.
+ */
+ final public function set(string $key, $value): void
+ {
+ $payload = $this->getPayload() ?? [];
+ $payload[$key] = $value;
+
+ $this->getStore()->put($this->getPrefix(), json_encode($payload, JSON_THROW_ON_ERROR));
+ }
+
+ /**
+ * Set the prefix used for all session keys.
+ *
+ * @param string $prefix Prefix to use for all session keys.
+ *
+ * @return $this
+ */
+ final public function setPrefix(
+ string $prefix = 'auth0',
+ ): self {
+ $prefix = trim($prefix);
+
+ if ('' === $prefix) {
+ throw new InvalidArgumentException('Prefix cannot be empty.');
+ }
+
+ $this->prefix = $prefix;
+
+ return $this;
+ }
+
+ protected function getPayload(): ?array
+ {
+ $encoded = $this->getStore()->get($this->getPrefix());
+
+ if (is_string($encoded)) {
+ $decoded = json_decode($encoded, true, 512);
+
+ if (is_array($decoded)) {
+ return $decoded;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieves the Laravel session store.
+ *
+ * @throws SessionException If a Laravel session store is not available.
+ *
+ * @psalm-suppress RedundantConditionGivenDocblockType
+ */
+ protected function getStore(): Store
+ {
+ $store = app('session.store');
+ $request = app('request');
+
+ if (! $request->hasSession(true)) {
+ $request->setLaravelSession($store);
+ }
+
+ if (! $store->isStarted()) {
+ $store->start();
+ }
+
+ return $store;
+ }
+}
diff --git a/src/Bridges/SessionBridgeContract.php b/src/Bridges/SessionBridgeContract.php
new file mode 100644
index 00000000..9d9c52af
--- /dev/null
+++ b/src/Bridges/SessionBridgeContract.php
@@ -0,0 +1,14 @@
+key;
- }
-
- /**
- * {@inheritdoc}
- */
- public function get(): mixed
- {
- return $this->isHit() ? $this->value : null;
- }
-
- /**
- * {@inheritdoc}
- */
- public function set($value = null): static
- {
- $this->value = $value;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function isHit(): bool
- {
- return $this->hit;
- }
-
- /**
- * {@inheritdoc}
- */
- public function expiresAt(?\DateTimeInterface $expiration): static
- {
- $this->expiration = $expiration ?? new \DateTimeImmutable('now +1 year');
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function expiresAfter(int|\DateInterval|null $time): static
- {
- $this->expiration = match (true) {
- null === $time => new \DateTimeImmutable('now +1 year'),
- \is_int($time) => new \DateTimeImmutable('now +' . $time . ' seconds'),
- $time instanceof \DateInterval => (new \DateTimeImmutable())->add($time), /* @phpstan-ignore-line */
- };
-
- return $this;
- }
-
- /**
- * Returns the expiration timestamp.
- */
- public function getExpiration(): \DateTimeInterface
- {
- return $this->expiration ?? new \DateTime('now +1 year');
- }
-
- /**
- * Returns the raw value, regardless of hit status.
- */
- public function getRawValue(): mixed
- {
- return $this->value;
- }
-
- /**
- * Return a LaravelCacheItem instance flagged as missed.
- */
- public static function miss(string $key): self
- {
- return new self(
- key: $key,
- value: null,
- hit: false,
- );
- }
-}
diff --git a/src/Cache/LaravelCachePool.php b/src/Cache/LaravelCachePool.php
deleted file mode 100644
index ce56e513..00000000
--- a/src/Cache/LaravelCachePool.php
+++ /dev/null
@@ -1,182 +0,0 @@
-
- */
- private array $deferred = [];
-
- public function getItem(string $key): CacheItemInterface
- {
- $value = $this->getStore()->
- get($key);
-
- if (false === $value) {
- return LaravelCacheItem::miss($key);
- }
-
- return $this->createItem($key, $value);
- }
-
- /**
- * @param string[] $keys
- * @return CacheItemInterface[]
- */
- public function getItems(array $keys = []): iterable
- {
- if ([] === $keys) {
- return [];
- }
-
- $results = $this->getStore()->
- many($keys);
- $items = [];
-
- foreach ($results as $key => $value) {
- $key = (string) $key;
- $items[$key] = $this->createItem($key, $value);
- }
-
- return $items;
- }
-
- /**
- * @param string $key the key for which to return the corresponding Cache Item
- */
- public function hasItem(mixed $key): bool
- {
- return $this->getItem($key)->
- isHit();
- }
-
- public function clear(): bool
- {
- $this->deferred = [];
-
- return $this->getStore()->
- flush();
- }
-
- /**
- * @param string $key the key for which to return the corresponding Cache Item
- */
- public function deleteItem(mixed $key): bool
- {
- return $this->getStore()->
- forget($key);
- }
-
- public function deleteItems(array $keys): bool
- {
- $deleted = true;
-
- foreach ($keys as $key) {
- if (! $this->deleteItem($key)) {
- $deleted = false;
- }
- }
-
- return $deleted;
- }
-
- public function save(CacheItemInterface $item): bool
- {
- if (! $item instanceof LaravelCacheItem) {
- return false;
- }
-
- $value = serialize($item->get());
- $key = $item->getKey();
- $expires = $item->getExpiration();
-
- if ($expires->getTimestamp() <= time()) {
- return $this->deleteItem($key);
- }
-
- $ttl = $expires->getTimestamp() - time();
-
- return $this->getStore()->put($key, $value, $ttl);
- }
-
- public function saveDeferred(CacheItemInterface $item): bool
- {
- if (! $item instanceof LaravelCacheItem) {
- return false;
- }
-
- $this->deferred[$item->getKey()] = [
- 'item' => $item,
- 'expiration' => $item->getExpiration(),
- ];
-
- return true;
- }
-
- public function commit(): bool
- {
- $success = true;
-
- foreach (array_keys($this->deferred) as $singleDeferred) {
- $item = $this->getDeferred((string) $singleDeferred);
-
- if (null !== $item && ! $this->save($item)) {
- $success = false;
- }
- }
-
- $this->deferred = [];
-
- return $success;
- }
-
- private function getStore(): \Illuminate\Contracts\Cache\Store
- {
- return app(\Illuminate\Cache\CacheManager::class)->getStore();
- }
-
- private function createItem(string $key, mixed $value): CacheItemInterface
- {
- if (! \is_string($value)) {
- return LaravelCacheItem::miss($key);
- }
-
- $value = unserialize($value);
-
- if (false === $value) {
- return LaravelCacheItem::miss($key);
- }
-
- return new LaravelCacheItem($key, $value, true);
- }
-
- private function getDeferred(string $key): ?CacheItemInterface
- {
- if (! isset($this->deferred[$key])) {
- return null;
- }
-
- $deferred = $this->deferred[$key];
- $item = clone $deferred['item'];
- $expires = $deferred['expiration'];
-
- if (null !== $expires && $expires <= time()) {
- unset($this->deferred[$key]);
-
- return null;
- }
-
- return $item;
- }
-}
diff --git a/src/Configuration.php b/src/Configuration.php
index ffb32b96..2180095c 100644
--- a/src/Configuration.php
+++ b/src/Configuration.php
@@ -4,38 +4,558 @@
namespace Auth0\Laravel;
+use Illuminate\Support\{Arr, Str};
+
+use function constant;
+use function count;
+use function defined;
+use function in_array;
+use function is_array;
+use function is_bool;
+use function is_int;
+use function is_string;
+
/**
* Helpers to map configuration data stored as strings from .env files into formats consumable by the Auth0-PHP SDK.
+ *
+ * @api
*/
-final class Configuration implements \Auth0\Laravel\Contract\Configuration
+final class Configuration implements ConfigurationContract
{
/**
- * {@inheritdoc}
+ * @var string[]
+ */
+ private const USES_ARRAYS = [
+ self::CONFIG_AUDIENCE,
+ self::CONFIG_SCOPE,
+ self::CONFIG_ORGANIZATION,
+ ];
+
+ /**
+ * @var string[]
*/
- public static function stringToArrayOrNull(?string $config, string $delimiter = ' '): ?array
+ private const USES_BOOLEANS = [
+ self::CONFIG_USE_PKCE,
+ self::CONFIG_HTTP_TELEMETRY,
+ self::CONFIG_COOKIE_SECURE,
+ self::CONFIG_PUSHED_AUTHORIZATION_REQUEST,
+ ];
+
+ /**
+ * @var string[]
+ */
+ private const USES_INTEGERS = [
+ self::CONFIG_TOKEN_MAX_AGE,
+ self::CONFIG_TOKEN_LEEWAY,
+ self::CONFIG_TOKEN_CACHE_TTL,
+ self::CONFIG_HTTP_MAX_RETRIES,
+ self::CONFIG_COOKIE_EXPIRES,
+ self::CONFIG_BACKCHANNEL_LOGOUT_EXPIRES,
+ ];
+
+ /**
+ * @var string
+ */
+ public const CONFIG_AUDIENCE = 'audience';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_BACKCHANNEL_LOGOUT_CACHE = 'backchannelLogoutCache';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_BACKCHANNEL_LOGOUT_EXPIRES = 'backchannelLogoutExpires';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_CLIENT_ASSERTION_SIGNING_ALGORITHM = 'clientAssertionSigningAlgorithm';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_CLIENT_ASSERTION_SIGNING_KEY = 'clientAssertionSigningKey';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_CLIENT_ID = 'clientId';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_CLIENT_SECRET = 'clientSecret';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_COOKIE_DOMAIN = 'cookieDomain';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_COOKIE_EXPIRES = 'cookieExpires';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_COOKIE_PATH = 'cookiePath';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_COOKIE_SAME_SITE = 'cookieSameSite';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_COOKIE_SECRET = 'cookieSecret';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_COOKIE_SECURE = 'cookieSecure';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_CUSTOM_DOMAIN = 'customDomain';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_DOMAIN = 'domain';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_HTTP_MAX_RETRIES = 'httpMaxRetries';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_HTTP_TELEMETRY = 'httpTelemetry';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_MANAGEMENT_TOKEN = 'managementToken';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_MANAGEMENT_TOKEN_CACHE = 'managementTokenCache';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_NAMESPACE = 'auth0.';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_NAMESPACE_ROUTES = 'auth0.routes.';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_ORGANIZATION = 'organization';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_PUSHED_AUTHORIZATION_REQUEST = 'pushedAuthorizationRequest';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_REDIRECT_URI = 'redirectUri';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_RESPONSE_MODE = 'responseMode';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_RESPONSE_TYPE = 'responseType';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_ROUTE_AFTER_LOGIN = 'afterLogin';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_ROUTE_AFTER_LOGOUT = 'afterLogout';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_ROUTE_BACKCHANNEL = 'backchannel';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_ROUTE_CALLBACK = 'callback';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_ROUTE_INDEX = 'index';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_ROUTE_LOGIN = 'login';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_ROUTE_LOGOUT = 'logout';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_SCOPE = 'scope';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_SESSION_STORAGE = 'sessionStorage';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_SESSION_STORAGE_ID = 'sessionStorageId';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_STRATEGY = 'strategy';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_TOKEN_ALGORITHM = 'tokenAlgorithm';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_TOKEN_CACHE = 'tokenCache';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_TOKEN_CACHE_TTL = 'tokenCacheTtl';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_TOKEN_JWKS_URI = 'tokenJwksUri';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_TOKEN_LEEWAY = 'tokenLeeway';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_TOKEN_MAX_AGE = 'tokenMaxAge';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_TRANSIENT_STORAGE = 'transientStorage';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_TRANSIENT_STORAGE_ID = 'transientStorageId';
+
+ /**
+ * @var string
+ */
+ public const CONFIG_USE_PKCE = 'usePkce';
+
+ /**
+ * @var array
+ */
+ public const VERSION_2 = ['AUTH0_CONFIG_VERSION' => 2];
+
+ private static ?array $environment = null;
+
+ private static ?array $json = null;
+
+ private static ?string $path = null;
+
+ public static function get(
+ string $setting,
+ array | string | int | bool | null $default = null,
+ ): array | string | int | bool | null {
+ if (in_array($setting, self::USES_ARRAYS, true)) {
+ $value = self::getValue($setting, $default);
+
+ if (! is_string($value)) {
+ return $default;
+ }
+
+ return self::stringToArrayOrNull($value, ',') ?? $default;
+ }
+
+ if (in_array($setting, self::USES_BOOLEANS, true)) {
+ $value = self::getValue($setting, $default);
+
+ if (! is_bool($value) && ! is_string($value)) {
+ return $default;
+ }
+
+ return self::stringToBoolOrNull($value) ?? $default;
+ }
+
+ if (in_array($setting, self::USES_INTEGERS, true)) {
+ $value = self::getValue($setting, $default);
+
+ if (! is_int($value) && ! is_string($value)) {
+ return $default;
+ }
+
+ return self::stringOrIntToIntOrNull($value) ?? $default;
+ }
+
+ $result = null;
+ $value = self::getValue($setting) ?? $default;
+
+ if (is_string($value) || is_int($value) || null === $value) {
+ $result = self::stringOrNull($value) ?? $default;
+ }
+
+ if (self::CONFIG_DOMAIN === $setting && null === $result) {
+ // Fallback to extracting the tenant domain from the signing key subject.
+ $temp = self::getJson()['signing_keys.0.subject'] ?? '';
+ $temp = explode('=', $temp);
+
+ if (isset($temp[1]) && str_ends_with($temp[1], '.auth0.com')) {
+ $result = $temp[1]; // @codeCoverageIgnore
+ }
+ }
+
+ return $result ?? $default;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ *
+ * @psalm-suppress DocblockTypeContradiction
+ */
+ public static function getEnvironment(): array
{
- if (\is_string($config) && '' !== $config && '' !== $delimiter) {
- $response = explode($delimiter, $config);
+ if (null === self::$environment) {
+ $path = self::getPath();
+ $laravelEnvironment = env('APP_ENV');
+ $laravelEnvironment = is_string($laravelEnvironment) && '' !== trim($laravelEnvironment) ? trim($laravelEnvironment) : 'local';
- // @phpstan-ignore-next-line
- if (\count($response) >= 1 && '' !== trim($response[0])) {
- return $response;
+ $env = [];
+ $files = ['.env', '.env.auth0'];
+ $files[] = '.env.' . $laravelEnvironment;
+ $files[] = '.env.auth0.' . $laravelEnvironment;
+
+ foreach ($files as $file) {
+ if (! file_exists($path . $file)) {
+ continue;
+ }
+
+ $contents = file($path . $file, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES);
+
+ if (! is_array($contents)) {
+ continue;
+ }
+
+ if ([] === $contents) {
+ continue;
+ }
+
+ foreach ($contents as $content) {
+ if (1 !== substr_count($content, '=')) {
+ continue;
+ }
+
+ [$k,$v] = explode('=', $content);
+
+ // @phpstan-ignore-next-line
+ if (! is_string($k)) {
+ continue;
+ }
+
+ // @phpstan-ignore-next-line
+ if (! is_string($v)) {
+ continue;
+ }
+
+ $v = trim($v);
+
+ if ('' === $v) {
+ $v = null;
+ } elseif ('empty' === $v) {
+ $v = null;
+ } elseif ('(empty)' === $v) {
+ $v = null;
+ } elseif ('null' === $v) {
+ $v = null;
+ } elseif ('(null)' === $v) {
+ $v = null;
+ } elseif ('true' === $v) {
+ $v = true;
+ } elseif ('(true)' === $v) {
+ $v = true;
+ } elseif ('false' === $v) {
+ $v = false;
+ } elseif ('(false)' === $v) {
+ $v = false;
+ }
+
+ $env[trim($k)] = $v;
+ }
}
+
+ self::$environment = $env;
}
- return null;
+ return self::$environment;
}
/**
- * {@inheritdoc}
+ * @codeCoverageIgnore
*/
- public static function stringToArray(?string $config, string $delimiter = ' '): array
+ public static function getJson(): array
+ {
+ if (null === self::$json) {
+ $path = self::getPath();
+ $laravelEnvironment = env('APP_ENV');
+ $laravelEnvironment = is_string($laravelEnvironment) && '' !== trim($laravelEnvironment) ? trim($laravelEnvironment) : 'local';
+
+ $configuration = [];
+ $files = ['.auth0.json', '.auth0.api.json', '.auth0.app.json'];
+ $files[] = '.auth0.' . $laravelEnvironment . '.json';
+ $files[] = '.auth0.' . $laravelEnvironment . '.api.json';
+ $files[] = '.auth0.' . $laravelEnvironment . '.app.json';
+
+ foreach ($files as $file) {
+ if (file_exists($path . $file)) {
+ $content = file_get_contents($path . $file);
+
+ if (is_string($content)) {
+ $json = json_decode($content, true, 512);
+
+ if (is_array($json)) {
+ $configuration = array_merge($configuration, $json);
+ }
+ }
+ }
+ }
+
+ self::$json = Arr::dot($configuration);
+ }
+
+ return self::$json;
+ }
+
+ public static function getPath(): string
+ {
+ if (null === self::$path) {
+ $path = config('auth0.configurationPath');
+
+ if (! is_string($path)) {
+ $path = base_path() . DIRECTORY_SEPARATOR;
+ }
+
+ self::$path = $path;
+ }
+
+ return self::$path;
+ }
+
+ public static function string(string $key, ?string $default = null): ?string
+ {
+ $value = config($key, $default);
+
+ if (is_string($value)) {
+ return $value;
+ }
+
+ return null;
+ }
+
+ public static function stringOrIntToIntOrNull(
+ int | string $value,
+ int | null $default = null,
+ ): int | null {
+ if (is_int($value)) {
+ return $value;
+ }
+
+ $value = trim($value);
+
+ if ('' === $value) {
+ return $default;
+ }
+
+ if (ctype_digit($value)) {
+ return (int) $value;
+ }
+
+ return $default;
+ }
+
+ public static function stringOrNull(
+ string | int | null $value,
+ string | int | null $default = null,
+ ): string | int | null {
+ if (! is_string($value)) {
+ return $default;
+ }
+
+ $value = trim($value);
+
+ if ('' === $value) {
+ return $default;
+ }
+
+ if ('empty' === $value) {
+ return $default;
+ }
+
+ if ('(empty)' === $value) {
+ return $default;
+ }
+
+ if ('null' === $value) {
+ return $default;
+ }
+
+ if ('(null)' === $value) {
+ return $default;
+ }
+
+ return $value;
+ }
+
+ public static function stringToArray(array | string | null $config, string $delimiter = ' '): array
{
- if (\is_string($config) && '' !== $config && '' !== $delimiter) {
+ if (is_array($config)) {
+ return $config;
+ }
+
+ if (is_string($config) && '' !== $config && '' !== $delimiter) {
$response = explode($delimiter, $config);
// @phpstan-ignore-next-line
- if (\count($response) >= 1 && '' !== trim($response[0])) {
+ if (count($response) >= 1 && '' !== trim($response[0])) {
return $response;
}
}
@@ -43,17 +563,95 @@ public static function stringToArray(?string $config, string $delimiter = ' '):
return [];
}
- /**
- * {@inheritdoc}
- */
- public static function stringToBoolOrNull(?string $config, ?bool $default = null): ?bool
+ public static function stringToArrayOrNull(array | string | null $config, string $delimiter = ' '): ?array
+ {
+ if (is_array($config) && [] !== $config) {
+ return $config;
+ }
+
+ if (is_string($config) && '' !== $config && '' !== $delimiter) {
+ $response = explode($delimiter, $config);
+
+ // @phpstan-ignore-next-line
+ if (count($response) >= 1 && '' !== trim($response[0])) {
+ return $response;
+ }
+ }
+
+ return null;
+ }
+
+ public static function stringToBoolOrNull(bool | string | null $config, ?bool $default = null): ?bool
{
- if (\is_string($config) && '' !== $config) {
- $config = mb_strtolower(trim($config));
+ if (is_bool($config)) {
+ return $config;
+ }
+
+ if (is_string($config) && '' !== $config) {
+ $config = strtolower(trim($config));
+
+ if ('true' === $config) {
+ return true;
+ }
- return 'true' === $config;
+ if ('false' === $config) {
+ return false;
+ }
}
return $default;
}
+
+ public static function version(): int
+ {
+ $version = config('auth0.AUTH0_CONFIG_VERSION', 1);
+
+ return is_int($version) ? $version : 1;
+ }
+
+ private static function getValue(
+ string $setting,
+ array | bool | string | int | null $default = null,
+ ): array | bool | string | int | null {
+ $value = null;
+
+ if (defined('AUTH0_OVERRIDE_CONFIGURATION')) {
+ $override = constant('AUTH0_OVERRIDE_CONFIGURATION');
+
+ if (is_string($override)) {
+ $value = config($override . '.' . $setting);
+ }
+ } else {
+ $env = 'AUTH0_' . strtoupper(Str::snake($setting));
+ $json = self::CONFIG_AUDIENCE === $setting ? 'identifier' : Str::snake($setting);
+
+ $value = getenv($env);
+
+ if (! is_string($value)) {
+ $value = null;
+ }
+
+ $value ??= self::getEnvironment()[$env] ?? null;
+
+ $value = match ($setting) {
+ self::CONFIG_DOMAIN => '{DOMAIN}' === $value ? null : $value,
+ self::CONFIG_CLIENT_ID => '{CLIENT_ID}' === $value ? null : $value,
+ self::CONFIG_CLIENT_SECRET => '{CLIENT_SECRET}' === $value ? null : $value,
+ self::CONFIG_AUDIENCE => '{API_IDENTIFIER}' === $value ? null : $value,
+ default => $value,
+ };
+
+ $value ??= self::getJson()[$json] ?? $default;
+ }
+
+ if (! is_string($value) && ! is_array($value) && ! is_bool($value) && ! is_int($value)) {
+ $value = null;
+ }
+
+ if (is_string($value)) {
+ $value = trim($value, '\'"');
+ }
+
+ return $value ?? $default;
+ }
}
diff --git a/src/ConfigurationContract.php b/src/ConfigurationContract.php
new file mode 100644
index 00000000..3c638880
--- /dev/null
+++ b/src/ConfigurationContract.php
@@ -0,0 +1,47 @@
+|string $config
+ * @param string $delimiter
+ */
+ public static function stringToArrayOrNull(array | string | null $config, string $delimiter = ' '): ?array;
+
+ /**
+ * Converts a truthy string representation into a boolean.
+ *
+ * @param null|bool|string $config
+ * @param ?bool $default
+ */
+ public static function stringToBoolOrNull(string | bool | null $config, ?bool $default = null): ?bool;
+}
diff --git a/src/Contract/Auth/Guard.php b/src/Contract/Auth/Guard.php
deleted file mode 100644
index 6bee220e..00000000
--- a/src/Contract/Auth/Guard.php
+++ /dev/null
@@ -1,45 +0,0 @@
-guard();
+
+ if (! $guard instanceof GuardAbstract) {
+ logger()->error(sprintf('A request implementing the `%s` controller was not routed through a Guard configured with an Auth0 driver. The incorrectly assigned Guard was: %s', self::class, $guard::class), $request->toArray());
+
+ throw new ControllerException(ControllerException::ROUTED_USING_INCOMPATIBLE_GUARD);
+ }
+
+ $code = $request->query('code');
+ $state = $request->query('state');
+ $code = is_string($code) ? trim($code) : '';
+ $state = is_string($state) ? trim($state) : '';
+ $success = false;
+
+ if ('' === $code) {
+ $code = null;
+ }
+
+ if ('' === $state) {
+ $state = null;
+ }
+
+ /**
+ * @var null|string $code
+ * @var null|string $state
+ */
+ try {
+ if (null !== $code && null !== $state) {
+ Events::framework(new Attempting($guard::class, ['code' => $code, 'state' => $state], true));
+
+ $success = $guard->sdk()->exchange(
+ code: $code,
+ state: $state,
+ );
+ }
+ } catch (Throwable $throwable) {
+ $credentials = $guard->sdk()->getUser() ?? [];
+ $credentials['code'] = $code;
+ $credentials['state'] = $state;
+ $credentials['error'] = ['description' => $throwable->getMessage()];
+
+ Events::framework(new Failed($guard::class, $guard->user(), $credentials));
+
+ session()->invalidate();
+
+ Events::dispatch($event = new AuthenticationFailed($throwable, true));
+
+ if ($event->throwException) {
+ throw $throwable;
+ }
+ }
+
+ if (null !== $request->query('error') && null !== $request->query('error_description')) {
+ // Workaround to aid static analysis, due to the mixed formatting of the query() response:
+ $error = $request->query('error', '');
+ $errorDescription = $request->query('error_description', '');
+ $error = is_string($error) ? $error : '';
+ $errorDescription = is_string($errorDescription) ? $errorDescription : '';
+
+ Events::framework(new Attempting($guard::class, ['code' => $code, 'state' => $state], true));
+
+ Events::framework(new Failed($guard::class, $guard->user(), [
+ 'code' => $code,
+ 'state' => $state,
+ 'error' => ['error' => $error, 'description' => $errorDescription],
+ ]));
+
+ session()->invalidate();
+
+ // Create a dynamic exception to report the API error response
+ $exception = new CallbackControllerException(sprintf(CallbackControllerException::MSG_API_RESPONSE, $error, $errorDescription));
+
+ // Store the API exception in the session as a flash variable, in case the application wants to access it.
+ session()->flash('auth0.callback.error', sprintf(CallbackControllerException::MSG_API_RESPONSE, $error, $errorDescription));
+
+ Events::dispatch($event = new AuthenticationFailed($exception, true));
+
+ if ($event->throwException) {
+ throw $exception;
+ }
+ }
+
+ if (! $success) {
+ return redirect()->intended(config(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_LOGIN, '/login'));
+ }
+
+ $credential = ($guard instanceof Guard) ? $guard->find(Guard::SOURCE_SESSION) : $guard->find();
+
+ $user = $credential?->getUser();
+
+ if ($credential instanceof CredentialEntityContract && $user instanceof Authenticatable) {
+ Events::framework(new Validated($guard::class, $user));
+
+ session()->regenerate(true);
+
+ /**
+ * @var Guard $guard
+ */
+ $guard->login($credential, Guard::SOURCE_SESSION);
+
+ Events::dispatch(new AuthenticationSucceeded($user));
+
+ // @phpstan-ignore-next-line
+ if ($user instanceof Authenticatable) {
+ Events::framework(new Authenticated($guard::class, $user));
+ }
+ }
+
+ return redirect()->intended(config(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_AFTER_LOGIN, config(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_INDEX, '/')));
+ }
+}
diff --git a/src/Controllers/CallbackControllerContract.php b/src/Controllers/CallbackControllerContract.php
new file mode 100644
index 00000000..90a16075
--- /dev/null
+++ b/src/Controllers/CallbackControllerContract.php
@@ -0,0 +1,18 @@
+guard();
+
+ if (! $guard instanceof GuardAbstract) {
+ logger()->error(sprintf('A request implementing the `%s` controller was not routed through a Guard configured with an Auth0 driver. The incorrectly assigned Guard was: %s', self::class, $guard::class), $request->toArray());
+
+ throw new ControllerException(ControllerException::ROUTED_USING_INCOMPATIBLE_GUARD);
+ }
+
+ $loggedIn = $guard->check() ? true : null;
+ $loggedIn ??= (($guard instanceof Guard) ? $guard->find(Guard::SOURCE_SESSION) : $guard->find()) instanceof CredentialEntityContract;
+
+ if ($loggedIn) {
+ return redirect()->intended(
+ config(
+ Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_AFTER_LOGIN,
+ config(
+ Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_INDEX,
+ '/',
+ ),
+ ),
+ );
+ }
+
+ session()->regenerate(true);
+
+ Events::dispatch($event = new LoginAttempting());
+
+ $url = $guard->sdk()->login(params: $event->parameters);
+
+ return redirect()->away($url);
+ }
+}
diff --git a/src/Controllers/LoginControllerContract.php b/src/Controllers/LoginControllerContract.php
new file mode 100644
index 00000000..d401ac6f
--- /dev/null
+++ b/src/Controllers/LoginControllerContract.php
@@ -0,0 +1,19 @@
+guard();
+
+ if (! $guard instanceof GuardAbstract) {
+ logger()->error(sprintf('A request implementing the `%s` controller was not routed through a Guard configured with an Auth0 driver. The incorrectly assigned Guard was: %s', self::class, $guard::class), $request->toArray());
+
+ throw new ControllerException(ControllerException::ROUTED_USING_INCOMPATIBLE_GUARD);
+ }
+
+ $loggedIn = $guard->check() ? true : null;
+ $loggedIn ??= (($guard instanceof Guard) ? $guard->find(Guard::SOURCE_SESSION) : $guard->find()) instanceof CredentialEntityContract;
+
+ $landing = Configuration::string(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_AFTER_LOGOUT);
+ $landing ??= Configuration::string(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_INDEX);
+ $landing ??= '/';
+
+ if ($loggedIn) {
+ session()->invalidate();
+
+ $guard->logout(); /** @phpstan-ignore-line */
+ $route = (string) url($landing); /** @phpstan-ignore-line */
+ $url = $guard->sdk()->authentication()->getLogoutLink($route);
+
+ return redirect()->away($url);
+ }
+
+ return redirect()->intended($landing);
+ }
+}
diff --git a/src/Controllers/LogoutControllerContract.php b/src/Controllers/LogoutControllerContract.php
new file mode 100644
index 00000000..098ba20b
--- /dev/null
+++ b/src/Controllers/LogoutControllerContract.php
@@ -0,0 +1,19 @@
+user = null;
+ $this->idToken = null;
+ $this->accessToken = null;
+ $this->accessTokenDecoded = null;
+ $this->accessTokenScope = null;
+ $this->accessTokenExpiration = null;
+ $this->refreshToken = null;
+
+ return $this;
+ }
+
+ public function setAccessToken(
+ ?string $accessToken = null,
+ ): self {
+ $this->accessToken = $accessToken;
+
+ return $this;
+ }
+
+ public function setAccessTokenDecoded(
+ ?array $accessTokenDecoded = null,
+ ): self {
+ $this->accessTokenDecoded = $accessTokenDecoded;
+
+ return $this;
+ }
+
+ public function setAccessTokenExpiration(
+ ?int $accessTokenExpiration = null,
+ ): self {
+ $this->accessTokenExpiration = $accessTokenExpiration;
+
+ return $this;
+ }
+
+ public function setAccessTokenScope(
+ ?array $accessTokenScope = null,
+ ): self {
+ $this->accessTokenScope = $accessTokenScope;
+
+ return $this;
+ }
+
+ public function setIdToken(
+ ?string $idToken = null,
+ ): self {
+ $this->idToken = $idToken;
+
+ return $this;
+ }
+
+ public function setRefreshToken(
+ ?string $refreshToken = null,
+ ): self {
+ $this->refreshToken = $refreshToken;
+
+ return $this;
+ }
+
+ public function setUser(
+ ?Authenticatable $user = null,
+ ): self {
+ $this->user = $user;
+
+ return $this;
+ }
+
+ /**
+ * Create a new Credential instance.
+ *
+ * @param null|Authenticatable $user The user entity this credential represents.
+ * @param null|string $idToken The ID token for this credential.
+ * @param null|string $accessToken The access token for this credential.
+ * @param null|array $accessTokenScope The access token scope for this credential.
+ * @param null|int $accessTokenExpiration The access token expiration for this credential.
+ * @param null|string $refreshToken The refresh token for this credential.
+ * @param null|array $accessTokenDecoded The decoded access token for this credential.
+ */
+ public static function create(
+ ?Authenticatable $user = null,
+ ?string $idToken = null,
+ ?string $accessToken = null,
+ ?array $accessTokenScope = null,
+ ?int $accessTokenExpiration = null,
+ ?string $refreshToken = null,
+ ?array $accessTokenDecoded = null,
+ ): self {
+ return new self(
+ $user,
+ $idToken,
+ $accessToken,
+ $accessTokenScope,
+ $accessTokenExpiration,
+ $refreshToken,
+ $accessTokenDecoded,
+ );
+ }
+}
diff --git a/src/Entities/CredentialEntityAbstract.php b/src/Entities/CredentialEntityAbstract.php
new file mode 100644
index 00000000..ca5c5b9c
--- /dev/null
+++ b/src/Entities/CredentialEntityAbstract.php
@@ -0,0 +1,132 @@
+ $accessTokenScope The access token scope for this credential.
+ * @param null|int $accessTokenExpiration The access token expiration for this credential.
+ * @param null|string $refreshToken The refresh token for this credential.
+ * @param null|array $accessTokenDecoded The decoded access token for this credential.
+ */
+ public function __construct(
+ protected ?Authenticatable $user = null,
+ protected ?string $idToken = null,
+ protected ?string $accessToken = null,
+ protected ?array $accessTokenScope = null,
+ protected ?int $accessTokenExpiration = null,
+ protected ?string $refreshToken = null,
+ protected ?array $accessTokenDecoded = null,
+ ) {
+ }
+
+ final public function getAccessToken(): ?string
+ {
+ return $this->accessToken;
+ }
+
+ /**
+ * @psalm-suppress MixedReturnTypeCoercion
+ */
+ final public function getAccessTokenDecoded(): ?array
+ {
+ return $this->accessTokenDecoded;
+ }
+
+ final public function getAccessTokenExpiration(): ?int
+ {
+ return $this->accessTokenExpiration;
+ }
+
+ final public function getAccessTokenExpired(): ?bool
+ {
+ $expires = $this->getAccessTokenExpiration();
+
+ if (null === $expires || $expires <= 0) {
+ return null;
+ }
+
+ return time() >= $expires;
+ }
+
+ /**
+ * @psalm-suppress MixedReturnTypeCoercion
+ */
+ final public function getAccessTokenScope(): ?array
+ {
+ return $this->accessTokenScope;
+ }
+
+ final public function getIdToken(): ?string
+ {
+ return $this->idToken;
+ }
+
+ final public function getRefreshToken(): ?string
+ {
+ return $this->refreshToken;
+ }
+
+ final public function getUser(): ?Authenticatable
+ {
+ return $this->user;
+ }
+
+ /**
+ * @return array{user: false|string, idToken: null|string, accessToken: null|string, accessTokenDecoded: null|array, accessTokenScope: null|array, accessTokenExpiration: null|int, accessTokenExpired: null|bool, refreshToken: null|string}
+ */
+ final public function jsonSerialize(): mixed
+ {
+ return [
+ 'user' => json_encode($this->getUser(), JSON_FORCE_OBJECT),
+ 'idToken' => $this->getIdToken(),
+ 'accessToken' => $this->getAccessToken(),
+ 'accessTokenDecoded' => $this->getAccessTokenDecoded(),
+ 'accessTokenScope' => $this->getAccessTokenScope(),
+ 'accessTokenExpiration' => $this->getAccessTokenExpiration(),
+ 'accessTokenExpired' => $this->getAccessTokenExpired(),
+ 'refreshToken' => $this->getRefreshToken(),
+ ];
+ }
+
+ abstract public function clear(): self;
+
+ abstract public function setAccessToken(
+ ?string $accessToken = null,
+ ): self;
+
+ abstract public function setAccessTokenDecoded(
+ ?array $accessTokenDecoded = null,
+ ): self;
+
+ abstract public function setAccessTokenExpiration(
+ ?int $accessTokenExpiration = null,
+ ): self;
+
+ abstract public function setAccessTokenScope(
+ ?array $accessTokenScope = null,
+ ): self;
+
+ abstract public function setIdToken(
+ ?string $idToken = null,
+ ): self;
+
+ abstract public function setRefreshToken(
+ ?string $refreshToken = null,
+ ): self;
+
+ abstract public function setUser(
+ ?Authenticatable $user = null,
+ ): self;
+}
diff --git a/src/Entities/CredentialEntityContract.php b/src/Entities/CredentialEntityContract.php
new file mode 100644
index 00000000..1e75fc93
--- /dev/null
+++ b/src/Entities/CredentialEntityContract.php
@@ -0,0 +1,125 @@
+
+ */
+ public function getAccessTokenDecoded(): ?array;
+
+ /**
+ * Get the access token expiration for this credential.
+ */
+ public function getAccessTokenExpiration(): ?int;
+
+ /**
+ * Check if the access token for this credential has expired.
+ */
+ public function getAccessTokenExpired(): ?bool;
+
+ /**
+ * Get the access token scope for this credential.
+ *
+ * @return null|array
+ */
+ public function getAccessTokenScope(): ?array;
+
+ /**
+ * Get the ID token for this credential.
+ */
+ public function getIdToken(): ?string;
+
+ /**
+ * Get the refresh token for this credential.
+ */
+ public function getRefreshToken(): ?string;
+
+ /**
+ * Get the user entity this credential represents.
+ */
+ public function getUser(): ?Authenticatable;
+
+ /**
+ * Set the access token for this credential.
+ *
+ * @param null|string $accessToken The access token for this credential.
+ */
+ public function setAccessToken(
+ ?string $accessToken = null,
+ ): self;
+
+ /**
+ * Set the decoded access token content for this credential.
+ *
+ * @param null|array $accessTokenDecoded The decoded access token content for this credential.
+ */
+ public function setAccessTokenDecoded(
+ ?array $accessTokenDecoded = null,
+ ): self;
+
+ /**
+ * Set the access token expiration for this credential.
+ *
+ * @param null|int $accessTokenExpiration The access token expiration for this credential.
+ */
+ public function setAccessTokenExpiration(
+ ?int $accessTokenExpiration = null,
+ ): self;
+
+ /**
+ * Set the access token scope for this credential.
+ *
+ * @param null|array $accessTokenScope The access token scope for this credential.
+ */
+ public function setAccessTokenScope(
+ ?array $accessTokenScope = null,
+ ): self;
+
+ /**
+ * Set the ID token for this credential.
+ *
+ * @param null|string $idToken The ID token for this credential.
+ */
+ public function setIdToken(
+ ?string $idToken = null,
+ ): self;
+
+ /**
+ * Set the refresh token for this credential.
+ *
+ * @param null|string $refreshToken The refresh token for this credential.
+ */
+ public function setRefreshToken(
+ ?string $refreshToken = null,
+ ): self;
+
+ /**
+ * Set the user entity this credential represents.
+ *
+ * @param null|Authenticatable $user The user entity this credential represents.
+ */
+ public function setUser(
+ ?Authenticatable $user = null,
+ ): self;
+}
diff --git a/src/Entities/EntityAbstract.php b/src/Entities/EntityAbstract.php
new file mode 100644
index 00000000..65d88867
--- /dev/null
+++ b/src/Entities/EntityAbstract.php
@@ -0,0 +1,12 @@
+configuration instanceof SdkConfiguration) {
+ $configuration = [];
+
+ if (2 === Configuration::version()) {
+ $defaultConfiguration = config('auth0.guards.default');
+ $guardConfiguration = [];
+
+ if (null !== $this->guardConfigurationKey && '' !== $this->guardConfigurationKey && 'default' !== $this->guardConfigurationKey) {
+ $guardConfiguration = config('auth0.guards.' . $this->guardConfigurationKey) ?? [];
+ }
+
+ if (is_array($defaultConfiguration) && [] !== $defaultConfiguration) {
+ $configuration = array_merge($configuration, array_filter($defaultConfiguration));
+ }
+
+ if (is_array($guardConfiguration) && [] !== $guardConfiguration) {
+ $configuration = array_merge($configuration, array_filter($guardConfiguration));
+ }
+ }
+
+ if (2 !== Configuration::version()) {
+ $configuration = config('auth0');
+
+ if (! is_array($configuration)) {
+ $configuration = [];
+ }
+ }
+
+ $this->configuration = $this->createConfiguration($configuration);
+ }
+
+ return $this->configuration;
+ }
+
+ final public function getCredentials(): ?object
+ {
+ return $this->getSdk()->getCredentials();
+ }
+
+ final public function getGuardConfigurationKey(): ?string
+ {
+ return $this->guardConfigurationKey;
+ }
+
+ final public function getSdk(): Auth0Interface
+ {
+ if (! $this->sdk instanceof Auth0Interface) {
+ return $this->setSdk(new Auth0($this->getConfiguration()));
+ }
+
+ return $this->sdk;
+ }
+
+ final public function management(): ManagementInterface
+ {
+ return $this->getSdk()->management();
+ }
+
+ final public function setGuardConfigurationKey(
+ ?string $guardConfigurationKey = null,
+ ): self {
+ $this->guardConfigurationKey = $guardConfigurationKey;
+
+ return $this;
+ }
+
+ final public function setSdk(Auth0Interface $sdk): Auth0Interface
+ {
+ $this->configuration = $sdk->configuration();
+ $this->sdk = $sdk;
+
+ $this->setSdkTelemetry();
+
+ return $this->sdk;
+ }
+
+ abstract public function reset(): self;
+
+ /**
+ * @param null|array|SdkConfiguration $configuration
+ */
+ abstract public function setConfiguration(
+ SdkConfiguration | array | null $configuration = null,
+ ): self;
+
+ protected function bootBackchannelLogoutCache(array $config): array
+ {
+ $backchannelLogoutCache = $config['backchannelLogoutCache'] ?? null;
+
+ if (false === $backchannelLogoutCache) {
+ unset($config['backchannelLogoutCache']);
+
+ return $config;
+ }
+
+ if (null === $backchannelLogoutCache) {
+ $backchannelLogoutCache = $this->getBackchannelLogoutCachePool();
+ }
+
+ if (is_string($backchannelLogoutCache)) {
+ $backchannelLogoutCache = app(trim($backchannelLogoutCache));
+ }
+
+ $config['backchannelLogoutCache'] = $backchannelLogoutCache instanceof CacheItemPoolInterface ? $backchannelLogoutCache : null;
+
+ return $config;
+ }
+
+ protected function bootManagementTokenCache(array $config): array
+ {
+ $managementTokenCache = $config['managementTokenCache'] ?? null;
+ $this->getManagementTokenCachePool();
+
+ // if (false === $managementTokenCache) {
+ // unset($config['managementTokenCache']);
+
+ // return $config;
+ // }
+
+ // if (null === $managementTokenCache) {
+ // $managementTokenCache = $this->getManagementTokenCachePool();
+ // }
+
+ // if (is_string($managementTokenCache)) {
+ // $managementTokenCache = app(trim($managementTokenCache));
+ // }
+
+ $config['managementTokenCache'] = $managementTokenCache instanceof CacheItemPoolInterface ? $managementTokenCache : null;
+
+ return $config;
+ }
+
+ protected function bootSessionStorage(array $config): array
+ {
+ $sessionStorage = $config['sessionStorage'] ?? null;
+ $sessionStorageId = $config['sessionStorageId'] ?? 'auth0_session';
+
+ if (false === $sessionStorage || 'cookie' === $sessionStorage) {
+ unset($config['sessionStorage']);
+
+ return $config;
+ }
+
+ if (null === $sessionStorage) {
+ $sessionStorage = app(SessionBridge::class, [
+ 'prefix' => $sessionStorageId,
+ ]);
+ }
+
+ if (is_string($sessionStorage)) {
+ $sessionStorage = app(trim($sessionStorage), [
+ 'prefix' => $sessionStorageId,
+ ]);
+ }
+
+ $config['sessionStorage'] = $sessionStorage instanceof StoreInterface ? $sessionStorage : null;
+
+ return $config;
+ }
+
+ protected function bootStrategy(array $config): array
+ {
+ $strategy = $config['strategy'] ?? SdkConfiguration::STRATEGY_REGULAR;
+
+ if (! is_string($strategy)) {
+ $strategy = SdkConfiguration::STRATEGY_REGULAR;
+ }
+
+ $config['strategy'] = $strategy;
+
+ return $config;
+ }
+
+ protected function bootTokenCache(array $config): array
+ {
+ $tokenCache = $config['tokenCache'] ?? null;
+
+ if (false === $tokenCache) {
+ unset($config['tokenCache']);
+
+ return $config;
+ }
+
+ if (null === $tokenCache) {
+ $tokenCache = $this->getTokenCachePool();
+ }
+
+ if (is_string($tokenCache)) {
+ $tokenCache = app(trim($tokenCache));
+ }
+
+ $config['tokenCache'] = $tokenCache instanceof CacheItemPoolInterface ? $tokenCache : null;
+
+ return $config;
+ }
+
+ protected function bootTransientStorage(array $config): array
+ {
+ $transientStorage = $config['transientStorage'] ?? null;
+ $transientStorageId = $config['transientStorageId'] ?? 'auth0_transient';
+
+ if (false === $transientStorage || 'cookie' === $transientStorage) {
+ unset($config['transientStorage']);
+
+ return $config;
+ }
+
+ if (null === $transientStorage) {
+ $transientStorage = app(SessionBridge::class, [
+ 'prefix' => $transientStorageId,
+ ]);
+ }
+
+ if (is_string($transientStorage)) {
+ $transientStorage = app(trim($transientStorage), [
+ 'prefix' => $transientStorageId,
+ ]);
+ }
+
+ $config['transientStorage'] = $transientStorage instanceof StoreInterface ? $transientStorage : null;
+
+ return $config;
+ }
+
+ protected function createConfiguration(
+ array $configuration,
+ ): SdkConfiguration {
+ Events::dispatch(new BuildingConfigurationEvent($configuration));
+
+ $configuration = $this->bootStrategy($configuration);
+ $configuration = $this->bootTokenCache($configuration);
+ $configuration = $this->bootManagementTokenCache($configuration);
+
+ if (in_array($configuration['strategy'], SdkConfiguration::STRATEGIES_USING_SESSIONS, true)) {
+ $configuration = $this->bootSessionStorage($configuration);
+ $configuration = $this->bootTransientStorage($configuration);
+ }
+
+ $sdkConfiguration = new SdkConfiguration($configuration);
+
+ Events::dispatch(new BuiltConfigurationEvent($sdkConfiguration));
+
+ return $sdkConfiguration;
+ }
+
+ protected function getBackchannelLogoutCachePool(): CacheItemPoolInterface
+ {
+ if (! $this->backchannelLogoutCachePool instanceof CacheItemPoolInterface) {
+ $this->backchannelLogoutCachePool = app(CacheBridge::class);
+ }
+
+ return $this->backchannelLogoutCachePool;
+ }
+
+ protected function getManagementTokenCachePool(): CacheItemPoolInterface
+ {
+ if (! $this->managementTokenCachePool instanceof CacheItemPoolInterface) {
+ $this->managementTokenCachePool = app(CacheBridge::class);
+ }
+
+ return $this->managementTokenCachePool;
+ }
+
+ protected function getTokenCachePool(): CacheItemPoolInterface
+ {
+ if (! $this->tokenCachePool instanceof CacheItemPoolInterface) {
+ $this->tokenCachePool = app(CacheBridge::class);
+ }
+
+ return $this->tokenCachePool;
+ }
+
+ /**
+ * Updates the Auth0 PHP SDK's telemetry to include the correct Laravel markers.
+ */
+ protected function setSdkTelemetry(): self
+ {
+ HttpTelemetry::setEnvProperty('Laravel', app()->version());
+ HttpTelemetry::setPackage('laravel', Service::VERSION);
+
+ return $this;
+ }
+}
diff --git a/src/Entities/InstanceEntityContract.php b/src/Entities/InstanceEntityContract.php
new file mode 100644
index 00000000..a808d667
--- /dev/null
+++ b/src/Entities/InstanceEntityContract.php
@@ -0,0 +1,49 @@
+|SdkConfiguration $configuration
+ */
+ public function setConfiguration(SdkConfiguration | array | null $configuration): self;
+
+ /**
+ * Create/return instance of the Auth0-PHP SDK.
+ *
+ * @param Auth0Interface $sdk
+ */
+ public function setSdk(Auth0Interface $sdk): Auth0Interface;
+}
diff --git a/src/Entities/InstanceEntityTrait.php b/src/Entities/InstanceEntityTrait.php
new file mode 100644
index 00000000..3c4de02a
--- /dev/null
+++ b/src/Entities/InstanceEntityTrait.php
@@ -0,0 +1,62 @@
+sdk, $this->configuration);
+
+ $this->sdk = null;
+ $this->configuration = null;
+
+ return $this;
+ }
+
+ /**
+ * @param null|array|SdkConfiguration $configuration
+ */
+ public function setConfiguration(
+ SdkConfiguration | array | null $configuration = null,
+ ): self {
+ if (is_array($configuration)) {
+ $configuration = $this->createConfiguration($configuration);
+ }
+
+ $this->configuration = $configuration;
+
+ if ($this->configuration instanceof SdkConfiguration && $this->sdk instanceof Auth0Interface) {
+ $this->sdk->setConfiguration($this->configuration);
+ }
+
+ return $this;
+ }
+
+ public static function create(
+ SdkConfiguration | array | null $configuration = null,
+ ?string $guardConfigurationName = null,
+ ): self {
+ $instance = new self();
+
+ if (null !== $guardConfigurationName) {
+ $instance->setGuardConfigurationKey($guardConfigurationName);
+ }
+
+ if (null !== $configuration) {
+ $instance->setConfiguration($configuration);
+ }
+
+ return $instance;
+ }
+}
diff --git a/src/Event/Auth0Event.php b/src/Event/Auth0Event.php
deleted file mode 100644
index e89d2ba4..00000000
--- a/src/Event/Auth0Event.php
+++ /dev/null
@@ -1,24 +0,0 @@
-mutated;
- }
-}
diff --git a/src/Event/Configuration/Built.php b/src/Event/Configuration/Built.php
deleted file mode 100644
index 5e851c93..00000000
--- a/src/Event/Configuration/Built.php
+++ /dev/null
@@ -1,35 +0,0 @@
-configuration = $configuration;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getConfiguration(): Configuration
- {
- return $this->configuration;
- }
-}
diff --git a/src/Event/Stateful/AuthenticationFailed.php b/src/Event/Stateful/AuthenticationFailed.php
deleted file mode 100644
index 87c9064f..00000000
--- a/src/Event/Stateful/AuthenticationFailed.php
+++ /dev/null
@@ -1,54 +0,0 @@
-exception = $exception;
- $this->mutated = true;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getException(): \Throwable
- {
- return $this->exception;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setThrowException(bool $throwException): self
- {
- $this->throwException = $throwException;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getThrowException(): bool
- {
- return $this->throwException;
- }
-}
diff --git a/src/Event/Stateful/AuthenticationSucceeded.php b/src/Event/Stateful/AuthenticationSucceeded.php
deleted file mode 100644
index 35d2eaac..00000000
--- a/src/Event/Stateful/AuthenticationSucceeded.php
+++ /dev/null
@@ -1,36 +0,0 @@
-user = $user;
- $this->mutated = true;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getUser(): Authenticatable
- {
- return $this->user;
- }
-}
diff --git a/src/Event/Stateful/TokenExpired.php b/src/Event/Stateful/TokenExpired.php
deleted file mode 100644
index 337f4b74..00000000
--- a/src/Event/Stateful/TokenExpired.php
+++ /dev/null
@@ -1,9 +0,0 @@
-token = $token;
- $this->mutated = true;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getToken(): string
- {
- return $this->token;
- }
-}
diff --git a/src/Event/Stateless/TokenVerificationFailed.php b/src/Event/Stateless/TokenVerificationFailed.php
deleted file mode 100644
index 5aa6fdac..00000000
--- a/src/Event/Stateless/TokenVerificationFailed.php
+++ /dev/null
@@ -1,35 +0,0 @@
-token;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getException(): Throwable
- {
- return $this->exception;
- }
-}
diff --git a/src/Event/Stateless/TokenVerificationSucceeded.php b/src/Event/Stateless/TokenVerificationSucceeded.php
deleted file mode 100644
index eb650742..00000000
--- a/src/Event/Stateless/TokenVerificationSucceeded.php
+++ /dev/null
@@ -1,33 +0,0 @@
-token;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getPayload(): array
- {
- return $this->payload;
- }
-}
diff --git a/src/Events.php b/src/Events.php
new file mode 100644
index 00000000..e558c5d6
--- /dev/null
+++ b/src/Events.php
@@ -0,0 +1,102 @@
+ event(self::getName($event), $event));
+
+ return;
+ }
+
+ event(self::getName($event), $event);
+ }
+ }
+
+ public static function framework(object $event): void
+ {
+ if (self::$enabled) {
+ if (self::withoutTelescopeRecording($event::class)) {
+ self::TELESCOPE::withoutRecording(static fn (): mixed => event($event));
+
+ return;
+ }
+
+ event($event);
+ }
+ }
+
+ private static function getName(EventContract $event): string
+ {
+ return match ($event::class) {
+ AuthenticationFailed::class => self::AUTHENTICATION_FAILED,
+ AuthenticationSucceeded::class => self::AUTHENTICATION_SUCCEEDED,
+ BuildingConfigurationEvent::class => self::CONFIGURATION_BUILDING,
+ BuiltConfigurationEvent::class => self::CONFIGURATION_BUILT,
+ LoginAttempting::class => self::LOGIN_ATTEMPTING,
+ StatefulMiddlewareRequest::class => self::MIDDLEWARE_STATEFUL_REQUEST,
+ StatelessMiddlewareRequest::class => self::MIDDLEWARE_STATELESS_REQUEST,
+ TokenExpired::class => self::TOKEN_EXPIRED,
+ TokenRefreshFailed::class => self::TOKEN_REFRESH_FAILED,
+ TokenRefreshSucceeded::class => self::TOKEN_REFRESH_SUCCEEDED,
+ TokenVerificationAttempting::class => self::TOKEN_VERIFICATION_ATTEMPTING,
+ TokenVerificationFailed::class => self::TOKEN_VERIFICATION_FAILED,
+ TokenVerificationSucceeded::class => self::TOKEN_VERIFICATION_SUCCEEDED,
+ default => $event::class,
+ };
+ }
+
+ private static function withoutTelescopeRecording(string $event): bool
+ {
+ if (! class_exists(self::TELESCOPE)) {
+ return false;
+ }
+
+ return match ($event) {
+ self::CONFIGURATION_BUILDING => true,
+ self::CONFIGURATION_BUILT => true,
+ default => false,
+ };
+ }
+}
diff --git a/src/Events/Auth0EventContract.php b/src/Events/Auth0EventContract.php
new file mode 100644
index 00000000..cb4e6950
--- /dev/null
+++ b/src/Events/Auth0EventContract.php
@@ -0,0 +1,12 @@
+ json_decode(json_encode($this->exception, JSON_THROW_ON_ERROR), true),
+ 'throwException' => $this->throwException,
+ ];
+ }
+}
diff --git a/src/Events/AuthenticationFailedContract.php b/src/Events/AuthenticationFailedContract.php
new file mode 100644
index 00000000..04e6ee2d
--- /dev/null
+++ b/src/Events/AuthenticationFailedContract.php
@@ -0,0 +1,16 @@
+, throwException: bool}
+ */
+ public function jsonSerialize(): array;
+}
diff --git a/src/Events/AuthenticationSucceeded.php b/src/Events/AuthenticationSucceeded.php
new file mode 100644
index 00000000..98228de1
--- /dev/null
+++ b/src/Events/AuthenticationSucceeded.php
@@ -0,0 +1,14 @@
+ json_decode(json_encode($this->user, JSON_THROW_ON_ERROR), true),
+ ];
+ }
+}
diff --git a/src/Events/AuthenticationSucceededContract.php b/src/Events/AuthenticationSucceededContract.php
new file mode 100644
index 00000000..8c9b82b3
--- /dev/null
+++ b/src/Events/AuthenticationSucceededContract.php
@@ -0,0 +1,16 @@
+}
+ */
+ public function jsonSerialize(): array;
+}
diff --git a/src/Events/Configuration/BuildingConfigurationEvent.php b/src/Events/Configuration/BuildingConfigurationEvent.php
new file mode 100644
index 00000000..79a70b0c
--- /dev/null
+++ b/src/Events/Configuration/BuildingConfigurationEvent.php
@@ -0,0 +1,14 @@
+ $configuration a configuration array for use with the Auth0-PHP SDK
+ */
+ public function __construct(
+ public array &$configuration,
+ ) {
+ }
+
+ /**
+ * @psalm-suppress LessSpecificImplementedReturnType
+ *
+ * @return array{configuration: mixed}
+ */
+ final public function jsonSerialize(): array
+ {
+ return [
+ 'configuration' => json_decode(json_encode($this->configuration, JSON_THROW_ON_ERROR), true),
+ ];
+ }
+}
diff --git a/src/Events/Configuration/BuildingConfigurationEventContract.php b/src/Events/Configuration/BuildingConfigurationEventContract.php
new file mode 100644
index 00000000..ef907390
--- /dev/null
+++ b/src/Events/Configuration/BuildingConfigurationEventContract.php
@@ -0,0 +1,18 @@
+}
+ */
+ public function jsonSerialize(): array;
+}
diff --git a/src/Events/Configuration/BuiltConfigurationEvent.php b/src/Events/Configuration/BuiltConfigurationEvent.php
new file mode 100644
index 00000000..8fd9db4a
--- /dev/null
+++ b/src/Events/Configuration/BuiltConfigurationEvent.php
@@ -0,0 +1,14 @@
+ json_decode(json_encode($this->configuration, JSON_THROW_ON_ERROR), true),
+ ];
+ }
+}
diff --git a/src/Events/Configuration/BuiltConfigurationEventContract.php b/src/Events/Configuration/BuiltConfigurationEventContract.php
new file mode 100644
index 00000000..45044467
--- /dev/null
+++ b/src/Events/Configuration/BuiltConfigurationEventContract.php
@@ -0,0 +1,18 @@
+}
+ */
+ public function jsonSerialize(): array;
+}
diff --git a/src/Events/EventAbstract.php b/src/Events/EventAbstract.php
new file mode 100644
index 00000000..847be779
--- /dev/null
+++ b/src/Events/EventAbstract.php
@@ -0,0 +1,16 @@
+ $parameters Additional API parameters to be sent with the authentication request.
+ */
+ public function __construct(
+ public array $parameters = [],
+ ) {
+ }
+
+ /**
+ * @psalm-suppress LessSpecificImplementedReturnType
+ *
+ * @return array{parameters: mixed}
+ */
+ final public function jsonSerialize(): array
+ {
+ return [
+ 'parameters' => json_decode(json_encode($this->parameters, JSON_THROW_ON_ERROR), true),
+ ];
+ }
+}
diff --git a/src/Events/LoginAttemptingContract.php b/src/Events/LoginAttemptingContract.php
new file mode 100644
index 00000000..83ed5305
--- /dev/null
+++ b/src/Events/LoginAttemptingContract.php
@@ -0,0 +1,16 @@
+}
+ */
+ public function jsonSerialize(): array;
+}
diff --git a/src/Events/Middleware/StatefulMiddlewareRequest.php b/src/Events/Middleware/StatefulMiddlewareRequest.php
new file mode 100644
index 00000000..62499a8e
--- /dev/null
+++ b/src/Events/Middleware/StatefulMiddlewareRequest.php
@@ -0,0 +1,14 @@
+ json_decode(json_encode($this->request, JSON_THROW_ON_ERROR), true),
+ 'guard' => json_decode(json_encode($this->guard, JSON_THROW_ON_ERROR), true),
+ ];
+ }
+}
diff --git a/src/Events/Middleware/StatefulMiddlewareRequestContract.php b/src/Events/Middleware/StatefulMiddlewareRequestContract.php
new file mode 100644
index 00000000..26bbc83a
--- /dev/null
+++ b/src/Events/Middleware/StatefulMiddlewareRequestContract.php
@@ -0,0 +1,20 @@
+, guard: array}
+ */
+ public function jsonSerialize(): array;
+}
diff --git a/src/Events/Middleware/StatelessMiddlewareRequest.php b/src/Events/Middleware/StatelessMiddlewareRequest.php
new file mode 100644
index 00000000..48940d38
--- /dev/null
+++ b/src/Events/Middleware/StatelessMiddlewareRequest.php
@@ -0,0 +1,14 @@
+ json_decode(json_encode($this->request, JSON_THROW_ON_ERROR), true),
+ 'guard' => json_decode(json_encode($this->guard, JSON_THROW_ON_ERROR), true),
+ ];
+ }
+}
diff --git a/src/Events/Middleware/StatelessMiddlewareRequestContract.php b/src/Events/Middleware/StatelessMiddlewareRequestContract.php
new file mode 100644
index 00000000..413eca75
--- /dev/null
+++ b/src/Events/Middleware/StatelessMiddlewareRequestContract.php
@@ -0,0 +1,20 @@
+, guard: array}
+ */
+ public function jsonSerialize(): array;
+}
diff --git a/src/Events/TokenExpired.php b/src/Events/TokenExpired.php
new file mode 100644
index 00000000..65cbd14f
--- /dev/null
+++ b/src/Events/TokenExpired.php
@@ -0,0 +1,14 @@
+ $this->token,
+ ];
+ }
+}
diff --git a/src/Events/TokenVerificationAttemptingContract.php b/src/Events/TokenVerificationAttemptingContract.php
new file mode 100644
index 00000000..cd2fd325
--- /dev/null
+++ b/src/Events/TokenVerificationAttemptingContract.php
@@ -0,0 +1,16 @@
+ $this->token,
+ 'exception' => json_decode(json_encode($this->exception, JSON_THROW_ON_ERROR), true),
+ 'throwException' => $this->throwException,
+ ];
+ }
+}
diff --git a/src/Events/TokenVerificationFailedContract.php b/src/Events/TokenVerificationFailedContract.php
new file mode 100644
index 00000000..9574a7fa
--- /dev/null
+++ b/src/Events/TokenVerificationFailedContract.php
@@ -0,0 +1,16 @@
+, throwException: bool}
+ */
+ public function jsonSerialize(): array;
+}
diff --git a/src/Events/TokenVerificationSucceeded.php b/src/Events/TokenVerificationSucceeded.php
new file mode 100644
index 00000000..48aafc82
--- /dev/null
+++ b/src/Events/TokenVerificationSucceeded.php
@@ -0,0 +1,14 @@
+ $this->token,
+ 'payload' => $this->payload,
+ ];
+ }
+}
diff --git a/src/Events/TokenVerificationSucceededContract.php b/src/Events/TokenVerificationSucceededContract.php
new file mode 100644
index 00000000..f98f14f7
--- /dev/null
+++ b/src/Events/TokenVerificationSucceededContract.php
@@ -0,0 +1,16 @@
+}
+ */
+ public function jsonSerialize(): array;
+}
diff --git a/src/EventsContract.php b/src/EventsContract.php
new file mode 100644
index 00000000..5ad8b9c9
--- /dev/null
+++ b/src/EventsContract.php
@@ -0,0 +1,118 @@
+
+ */
+ public const AUTHENTICATION_FAILED = AuthenticationFailed::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\AuthenticationSucceeded>
+ */
+ public const AUTHENTICATION_SUCCEEDED = AuthenticationSucceeded::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\Configuration\BuildingConfigurationEvent>
+ */
+ public const CONFIGURATION_BUILDING = BuildingConfigurationEvent::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\Configuration\BuiltConfigurationEvent>
+ */
+ public const CONFIGURATION_BUILT = BuiltConfigurationEvent::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\LoginAttempting>
+ */
+ public const LOGIN_ATTEMPTING = LoginAttempting::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\Middleware\StatefulMiddlewareRequest>
+ */
+ public const MIDDLEWARE_STATEFUL_REQUEST = StatefulMiddlewareRequest::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\Middleware\StatelessMiddlewareRequest>
+ */
+ public const MIDDLEWARE_STATELESS_REQUEST = StatelessMiddlewareRequest::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\TokenExpired>
+ */
+ public const TOKEN_EXPIRED = TokenExpired::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\TokenRefreshFailed>
+ */
+ public const TOKEN_REFRESH_FAILED = TokenRefreshFailed::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\TokenRefreshSucceeded>
+ */
+ public const TOKEN_REFRESH_SUCCEEDED = TokenRefreshSucceeded::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\TokenVerificationAttempting>
+ */
+ public const TOKEN_VERIFICATION_ATTEMPTING = TokenVerificationAttempting::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\TokenVerificationFailed>
+ */
+ public const TOKEN_VERIFICATION_FAILED = TokenVerificationFailed::class;
+
+ /**
+ * @var class-string<\Auth0\Laravel\Events\TokenVerificationSucceeded>
+ */
+ public const TOKEN_VERIFICATION_SUCCEEDED = TokenVerificationSucceeded::class;
+
+ /**
+ * Dispatch an SDK event.
+ *
+ * @param EventContract $event The event to dispatch.
+ */
+ public static function dispatch(
+ EventContract $event,
+ ): void;
+
+ /**
+ * Dispatch a Laravel framework event.
+ *
+ * @param object $event The event to dispatch.
+ */
+ public static function framework(
+ object $event,
+ ): void;
+}
diff --git a/src/Exception/Stateful/CallbackException.php b/src/Exception/Stateful/CallbackException.php
deleted file mode 100644
index 7f3f9f58..00000000
--- a/src/Exception/Stateful/CallbackException.php
+++ /dev/null
@@ -1,21 +0,0 @@
-isImpersonating()) {
+ return $this->getImposter();
+ }
+
+ return $this->findSession();
+ }
+
+ public function findSession(): ?CredentialEntityContract
+ {
+ if ($this->isImpersonating()) {
+ return $this->getImposter();
+ }
+
+ $this->getSession();
+ $session = $this->pullState();
+ $user = $session?->getUser();
+
+ if ($session instanceof CredentialEntityContract && $user instanceof Authenticatable) {
+ $user = $this->getProvider()->retrieveByCredentials($this->normalizeUserArray($user));
+
+ if ($user instanceof Authenticatable) {
+ $scope = $session->getAccessTokenScope();
+ $decoded = $session->getAccessTokenDecoded();
+
+ /**
+ * @var array $scope
+ * @var array $decoded
+ */
+ $credential = CredentialEntity::create(
+ user: $user,
+ idToken: $session->getIdToken(),
+ accessToken: $session->getAccessToken(),
+ accessTokenDecoded: $decoded,
+ accessTokenScope: $scope,
+ accessTokenExpiration: $session->getAccessTokenExpiration(),
+ refreshToken: $session->getRefreshToken(),
+ );
+
+ return $this->refreshSession($credential);
+ }
+ }
+
+ return null;
+ }
+
+ public function forgetUser(): self
+ {
+ $this->setCredential();
+
+ return $this;
+ }
+
+ public function getCredential(): ?CredentialEntityContract
+ {
+ if ($this->isImpersonating()) {
+ return $this->getImposter();
+ }
+
+ if ($this->credential instanceof CredentialEntityContract) {
+ $updated = $this->findSession();
+ $this->setCredential($updated);
+ $this->pushState($updated);
+ }
+
+ return $this->credential;
+ }
+
+ public function login(
+ ?CredentialEntityContract $credential,
+ ): self {
+ $this->stopImpersonating();
+
+ $this->setCredential($credential);
+ $this->pushState($credential);
+
+ if ($credential instanceof CredentialEntityContract) {
+ $user = $credential->getUser();
+
+ if ($user instanceof Authenticatable) {
+ Events::framework(new Login(self::class, $user, true));
+ }
+ }
+
+ return $this;
+ }
+
+ public function logout(): self
+ {
+ $user = $this->user();
+
+ if ($user instanceof Authenticatable) {
+ Events::framework(new Logout(self::class, $user));
+ }
+
+ $this->stopImpersonating();
+ $this->setCredential();
+ $this->pushState();
+ $this->forgetUser();
+
+ return $this;
+ }
+
+ public function pushState(
+ ?CredentialEntityContract $credential = null,
+ ): self {
+ $sdk = $this->sdk();
+ $credential ??= $this->getCredential();
+
+ if (! $credential instanceof CredentialEntityContract) {
+ $sdk->clear(true);
+
+ return $this;
+ }
+
+ $user = $credential->getUser();
+ $idToken = $credential->getIdToken();
+ $accessToken = $credential->getAccessToken();
+ $accessTokenScope = $credential->getAccessTokenScope();
+ $accessTokenExpiration = $credential->getAccessTokenExpiration();
+ $refreshToken = $credential->getRefreshToken();
+
+ if ($user instanceof Authenticatable) {
+ $update = $this->normalizeUserArray($user);
+ $current = $sdk->getUser() ?? [];
+
+ if (count($update) !== count($current) || [] !== array_diff(array_map('serialize', $update), array_map('serialize', $current))) {
+ $sdk->setUser($update);
+ }
+ }
+
+ if (null !== $idToken && $idToken !== $sdk->getIdToken()) {
+ $sdk->setIdToken($idToken);
+ }
+
+ if (null !== $accessToken && $accessToken !== $sdk->getAccessToken()) {
+ $sdk->setAccessToken($accessToken);
+ }
+
+ if (null !== $accessTokenScope && $accessTokenScope !== $sdk->getAccessTokenScope()) {
+ /**
+ * @var array $accessTokenScope
+ */
+ $sdk->setAccessTokenScope($accessTokenScope);
+ }
+
+ if (null !== $accessTokenExpiration && $accessTokenExpiration !== $sdk->getAccessTokenExpiration()) {
+ $sdk->setAccessTokenExpiration($accessTokenExpiration);
+ }
+
+ if (null !== $refreshToken && $refreshToken !== $sdk->getRefreshToken()) {
+ $sdk->setRefreshToken($refreshToken);
+ }
+
+ return $this;
+ }
+
+ public function refreshUser(): void
+ {
+ if ($this->isImpersonating()) {
+ return;
+ }
+
+ if ($this->check()) {
+ $credential = $this->getCredential();
+ $accessToken = $credential?->getAccessToken();
+
+ if (! $credential instanceof CredentialEntityContract || null === $accessToken) {
+ return;
+ }
+
+ $response = $this->sdk()->authentication()->userInfo($accessToken);
+
+ if (HttpResponse::wasSuccessful($response)) {
+ $response = HttpResponse::decodeContent($response);
+
+ if (! is_array($response)) {
+ return;
+ }
+
+ $user = $this->getProvider()->retrieveByCredentials($response);
+ $scope = $credential->getAccessTokenScope();
+ $decoded = $credential->getAccessTokenDecoded();
+
+ /**
+ * @var array $scope
+ * @var array $decoded
+ */
+ $this->pushState(CredentialEntity::create(
+ user: $user,
+ idToken: $credential->getIdToken(),
+ accessToken: $credential->getAccessToken(),
+ accessTokenScope: $scope,
+ accessTokenDecoded: $decoded,
+ accessTokenExpiration: $credential->getAccessTokenExpiration(),
+ refreshToken: $credential->getRefreshToken(),
+ ));
+ }
+ }
+ }
+
+ public function setCredential(
+ ?CredentialEntityContract $credential = null,
+ ): self {
+ $this->stopImpersonating();
+
+ $this->credential = $credential;
+
+ return $this;
+ }
+
+ /**
+ * @param CredentialEntityContract $credential
+ */
+ public function setImpersonating(
+ CredentialEntityContract $credential,
+ ): self {
+ $this->impersonationSource = self::SOURCE_SESSION;
+ $this->impersonating = $credential;
+
+ return $this;
+ }
+
+ public function setUser(
+ Authenticatable $user,
+ ): self {
+ if ($this->isImpersonating()) {
+ if ($this->getImposter()?->getUser() === $user) {
+ return $this;
+ }
+
+ $this->stopImpersonating();
+ }
+
+ $credential = $this->getCredential() ?? CredentialEntity::create();
+ $credential->setUser($user);
+
+ $this->setCredential($credential);
+ $this->pushState($credential);
+
+ return $this;
+ }
+
+ public function user(): ?Authenticatable
+ {
+ if ($this->isImpersonating()) {
+ return $this->getImposter()?->getUser();
+ }
+
+ static $lastResponse = null;
+
+ /**
+ * @var ?Authenticatable $lastResponse
+ */
+ // @codeCoverageIgnoreStart
+ if (class_exists(self::TELESCOPE) && true === config('telescope.enabled')) {
+ static $depth = 0;
+ static $lastCalled = null;
+
+ /**
+ * @var int $depth
+ * @var ?int $lastCalled
+ */
+ if (null === $lastCalled) {
+ $lastCalled = time();
+ }
+
+ if (time() - $lastCalled > 10) {
+ $lastResponse = null;
+ $depth = 0;
+ }
+
+ if ($depth >= 1) {
+ return $lastResponse;
+ }
+
+ ++$depth;
+ $lastCalled = time();
+ }
+ // @codeCoverageIgnoreEnd
+
+ $currentUser = $this->getCredential()?->getUser();
+
+ if ($currentUser instanceof Authenticatable) {
+ return $lastResponse = $currentUser;
+ }
+
+ $session = $this->find();
+
+ if ($session instanceof CredentialEntityContract) {
+ $this->login($session);
+
+ return $lastResponse = $this->getCredential()?->getUser();
+ }
+
+ return $lastResponse = null;
+ }
+
+ private function pullState(): ?CredentialEntityContract
+ {
+ $sdk = $this->sdk();
+ $sdk->refreshState();
+
+ $credentials = $sdk->getCredentials();
+
+ /** @var mixed $credentials */
+ if (is_object($credentials) && property_exists($credentials, 'user') && property_exists($credentials, 'idToken') && property_exists($credentials, 'accessToken') && property_exists($credentials, 'accessTokenScope') && property_exists($credentials, 'accessTokenExpiration') && property_exists($credentials, 'refreshToken')) {
+ $decoded = null;
+
+ if (null !== $credentials->accessToken) {
+ $decoded = (new Parser(new SdkConfiguration(strategy: SdkConfiguration::STRATEGY_NONE), $credentials->accessToken))->export();
+ }
+
+ /**
+ * @var null|array $decoded
+ */
+
+ return CredentialEntity::create(
+ user: new StatefulUser($credentials->user),
+ idToken: $credentials->idToken,
+ accessToken: $credentials->accessToken,
+ accessTokenDecoded: $decoded,
+ accessTokenScope: $credentials->accessTokenScope,
+ accessTokenExpiration: $credentials->accessTokenExpiration,
+ refreshToken: $credentials->refreshToken,
+ );
+ }
+
+ return null;
+ }
+
+ private function refreshSession(
+ ?CredentialEntityContract $credential,
+ ): ?CredentialEntityContract {
+ if (! $credential instanceof CredentialEntityContract || true !== $credential->getAccessTokenExpired()) {
+ return $credential;
+ }
+
+ if (null === $credential->getRefreshToken()) {
+ return null;
+ }
+
+ try {
+ $this->sdk()->renew();
+ $session = $this->pullState();
+ } catch (Throwable) {
+ Events::dispatch(new TokenRefreshFailed());
+ $session = null;
+ }
+
+ if ($session instanceof CredentialEntityContract) {
+ Events::dispatch(new TokenRefreshSucceeded());
+ $user = $session->getUser();
+
+ // @codeCoverageIgnoreStart
+ if (! $user instanceof Authenticatable) {
+ return null;
+ }
+ // @codeCoverageIgnoreEnd
+
+ $user = $this->getProvider()->retrieveByCredentials($this->normalizeUserArray($user));
+
+ if ($user instanceof Authenticatable) {
+ $decoded = null;
+ $accessToken = $session->getAccessToken();
+
+ if (null !== $accessToken) {
+ $decoded = (new Parser(new SdkConfiguration(strategy: SdkConfiguration::STRATEGY_NONE), $accessToken))->export();
+ }
+
+ $scope = $session->getAccessTokenScope();
+
+ /**
+ * @var array $scope
+ * @var null|array $decoded
+ */
+
+ return CredentialEntity::create(
+ user: $user,
+ idToken: $session->getIdToken(),
+ accessToken: $session->getAccessToken(),
+ accessTokenDecoded: $decoded,
+ accessTokenScope: $scope,
+ accessTokenExpiration: $session->getAccessTokenExpiration(),
+ refreshToken: $session->getRefreshToken(),
+ );
+ }
+ }
+
+ $this->setCredential(null);
+ $this->pushState();
+
+ return null;
+ }
+}
diff --git a/src/Guards/AuthenticationGuardContract.php b/src/Guards/AuthenticationGuardContract.php
new file mode 100644
index 00000000..65d857b1
--- /dev/null
+++ b/src/Guards/AuthenticationGuardContract.php
@@ -0,0 +1,37 @@
+isImpersonating()) {
+ return $this->getImposter();
+ }
+
+ return $this->findToken();
+ }
+
+ public function findToken(): ?CredentialEntityContract
+ {
+ if ($this->isImpersonating()) {
+ return $this->getImposter();
+ }
+
+ $token = trim(app('request')->bearerToken() ?? '');
+
+ if ('' === $token) {
+ return null;
+ }
+
+ $decoded = $this->processToken(
+ token: $token,
+ );
+
+ /**
+ * @var null|array $decoded
+ */
+ if (null === $decoded) {
+ return null;
+ }
+
+ $provider = $this->getProvider();
+
+ // @codeCoverageIgnoreStart
+ if (! $provider instanceof UserProviderContract) {
+ return null;
+ }
+ // @codeCoverageIgnoreEnd
+
+ $user = $provider->getRepository()->fromAccessToken(
+ user: $decoded,
+ );
+
+ // @codeCoverageIgnoreStart
+ if (! $user instanceof Authenticatable) {
+ return null;
+ }
+ // @codeCoverageIgnoreEnd
+
+ $data = $this->normalizeUserArray($user);
+
+ // @codeCoverageIgnoreStart
+ if ([] === $data) {
+ return null;
+ }
+ // @codeCoverageIgnoreEnd
+
+ $scope = isset($data['scope']) && is_string($data['scope']) ? explode(' ', $data['scope']) : [];
+ $exp = isset($data['exp']) && is_numeric($data['exp']) ? (int) $data['exp'] : null;
+
+ return CredentialEntity::create(
+ user: $user,
+ accessToken: $token,
+ accessTokenScope: $scope,
+ accessTokenExpiration: $exp,
+ accessTokenDecoded: $decoded,
+ );
+ }
+
+ public function forgetUser(): self
+ {
+ $this->setCredential();
+
+ return $this;
+ }
+
+ public function getCredential(): ?CredentialEntityContract
+ {
+ if ($this->isImpersonating()) {
+ return $this->getImposter();
+ }
+
+ return $this->credential;
+ }
+
+ public function login(
+ ?CredentialEntityContract $credential,
+ ): self {
+ $this->stopImpersonating();
+
+ $this->setCredential($credential);
+
+ return $this;
+ }
+
+ public function logout(): self
+ {
+ $this->stopImpersonating();
+
+ $this->setCredential();
+ $this->forgetUser();
+
+ return $this;
+ }
+
+ public function refreshUser(): void
+ {
+ if ($this->isImpersonating()) {
+ return;
+ }
+
+ if ($this->check()) {
+ $credential = $this->getCredential();
+ $accessToken = $credential?->getAccessToken();
+
+ if (! $credential instanceof CredentialEntityContract || null === $accessToken) {
+ return;
+ }
+
+ $response = $this->sdk()->authentication()->userInfo($accessToken);
+
+ if (HttpResponse::wasSuccessful($response)) {
+ $response = HttpResponse::decodeContent($response);
+
+ if (! is_array($response)) {
+ return;
+ }
+
+ $user = $this->getProvider()->retrieveByCredentials($response);
+ $scope = $credential->getAccessTokenScope();
+
+ /**
+ * @var array $scope
+ */
+ $this->setCredential(CredentialEntity::create(
+ user: $user,
+ idToken: $credential->getIdToken(),
+ accessToken: $credential->getAccessToken(),
+ accessTokenScope: $scope,
+ accessTokenExpiration: $credential->getAccessTokenExpiration(),
+ refreshToken: $credential->getRefreshToken(),
+ ));
+ }
+ }
+ }
+
+ public function setCredential(
+ ?CredentialEntityContract $credential = null,
+ ): self {
+ $this->stopImpersonating();
+
+ $this->credential = $credential;
+
+ return $this;
+ }
+
+ /**
+ * @param CredentialEntityContract $credential
+ */
+ public function setImpersonating(
+ CredentialEntityContract $credential,
+ ): self {
+ $this->impersonationSource = self::SOURCE_TOKEN;
+ $this->impersonating = $credential;
+
+ return $this;
+ }
+
+ public function setUser(
+ Authenticatable $user,
+ ): self {
+ if ($this->isImpersonating()) {
+ if ($this->getImposter()?->getUser() === $user) {
+ return $this;
+ }
+
+ $this->stopImpersonating();
+ }
+
+ $credential = $this->getCredential() ?? CredentialEntity::create();
+ $credential->setUser($user);
+
+ $this->setCredential($credential);
+
+ return $this;
+ }
+
+ public function user(): ?Authenticatable
+ {
+ if ($this->isImpersonating()) {
+ return $this->getImposter()?->getUser();
+ }
+
+ $currentUser = $this->getCredential()?->getUser();
+
+ if ($currentUser instanceof Authenticatable) {
+ return $currentUser;
+ }
+
+ $token = $this->find();
+
+ if ($token instanceof CredentialEntityContract) {
+ $this->login($token);
+
+ return $this->getCredential()?->getUser();
+ }
+
+ return null;
+ }
+}
diff --git a/src/Guards/AuthorizationGuardContract.php b/src/Guards/AuthorizationGuardContract.php
new file mode 100644
index 00000000..8b762eef
--- /dev/null
+++ b/src/Guards/AuthorizationGuardContract.php
@@ -0,0 +1,33 @@
+user()) instanceof Authenticatable) {
+ return $user;
+ }
+
+ throw new AuthenticationException(AuthenticationException::UNAUTHENTICATED);
+ }
+
+ final public function check(): bool
+ {
+ return $this->hasUser();
+ }
+
+ final public function getImposter(): ?CredentialEntityContract
+ {
+ return $this->impersonating;
+ }
+
+ final public function getImposterSource(): ?int
+ {
+ return $this->impersonationSource;
+ }
+
+ final public function getName(): string
+ {
+ return $this->name;
+ }
+
+ final public function getProvider(): UserProvider
+ {
+ if ($this->provider instanceof UserProvider) {
+ return $this->provider;
+ }
+
+ $providerName = $this->config['provider'] ?? '';
+
+ if (! is_string($providerName) || '' === $providerName) {
+ // @codeCoverageIgnoreStart
+ throw new GuardException(GuardExceptionContract::USER_PROVIDER_UNCONFIGURED);
+ // @codeCoverageIgnoreEnd
+ }
+
+ $providerName = trim($providerName);
+ $provider = app('auth')->createUserProvider($providerName);
+
+ if ($provider instanceof UserProvider) {
+ $this->provider = $provider;
+
+ return $provider;
+ }
+
+ // @codeCoverageIgnoreStart
+ throw new GuardException(sprintf(GuardExceptionContract::USER_PROVIDER_UNAVAILABLE, $providerName));
+ // @codeCoverageIgnoreEnd
+ }
+
+ final public function getRefreshedUser(): ?Authenticatable
+ {
+ $this->refreshUser();
+
+ return $this->user();
+ }
+
+ final public function getSession(): Session
+ {
+ if (! $this->session instanceof Session) {
+ $store = app('session.store');
+ $request = app('request');
+
+ if (! $request->hasSession(true)) {
+ $request->setLaravelSession($store);
+ }
+
+ if (! $store->isStarted()) {
+ $store->start();
+ }
+
+ $this->session = $store;
+ }
+
+ return $this->session;
+ }
+
+ final public function guest(): bool
+ {
+ return ! $this->check();
+ }
+
+ final public function hasPermission(
+ string $permission,
+ ?CredentialEntityContract $credential = null,
+ ): bool {
+ $permission = trim($permission);
+
+ if ('*' === $permission) {
+ return true;
+ }
+
+ $available = $credential?->getAccessTokenDecoded() ?? $this->getCredential()?->getAccessTokenDecoded() ?? [];
+ $available = $available['permissions'] ?? [];
+
+ /**
+ * @var mixed $available
+ */
+ if (! is_array($available) || [] === $available) {
+ return false;
+ }
+
+ return in_array($permission, $available, true);
+ }
+
+ final public function hasScope(
+ string $scope,
+ ?CredentialEntityContract $credential = null,
+ ): bool {
+ $scope = trim($scope);
+
+ if ('*' === $scope) {
+ return true;
+ }
+
+ $available = $credential?->getAccessTokenScope() ?? $this->getCredential()?->getAccessTokenScope() ?? [];
+
+ if ([] !== $available) {
+ return in_array($scope, $available, true);
+ }
+
+ return false;
+ }
+
+ final public function hasUser(): bool
+ {
+ return $this->user() instanceof Authenticatable;
+ }
+
+ final public function id(): string | null
+ {
+ $user = $this->user()?->getAuthIdentifier();
+
+ if (is_string($user) || is_int($user)) {
+ return (string) $user;
+ }
+
+ return null;
+ }
+
+ final public function isImpersonating(): bool
+ {
+ return $this->impersonating instanceof CredentialEntityContract;
+ }
+
+ final public function management(): ManagementInterface
+ {
+ return $this->sdk()->management();
+ }
+
+ final public function processToken(
+ string $token,
+ ): ?array {
+ Events::dispatch($event = new TokenVerificationAttempting($token));
+ $token = $event->token;
+ $decoded = null;
+
+ try {
+ $decoded = $this->sdk()->decode(token: $token, tokenType: Token::TYPE_ACCESS_TOKEN)->toArray();
+ } catch (InvalidTokenException $invalidTokenException) {
+ Events::dispatch($event = new TokenVerificationFailed($token, $invalidTokenException));
+
+ if ($event->throwException) {
+ // @codeCoverageIgnoreStart
+ throw $invalidTokenException;
+ // @codeCoverageIgnoreEnd
+ }
+
+ return null;
+ }
+
+ Events::dispatch(new TokenVerificationSucceeded($token, $decoded));
+
+ return $decoded;
+ }
+
+ final public function sdk(
+ bool $reset = false,
+ ): Auth0Interface {
+ if (! $this->sdk instanceof InstanceEntityContract || $reset) {
+ $configurationName = $this->config['configuration'] ?? $this->name;
+
+ $this->sdk = InstanceEntity::create(
+ guardConfigurationName: $configurationName,
+ );
+ }
+
+ return $this->sdk->getSdk();
+ }
+
+ /**
+ * @codeCoverageIgnore
+ */
+ final public function service(): ?InstanceEntityContract
+ {
+ return $this->sdk;
+ }
+
+ final public function stopImpersonating(): void
+ {
+ $this->impersonating = null;
+ $this->impersonationSource = null;
+ }
+
+ /**
+ * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
+ *
+ * @param array $credentials
+ */
+ final public function validate(
+ array $credentials = [],
+ ): bool {
+ return false;
+ }
+
+ final public function viaRemember(): bool
+ {
+ return false;
+ }
+
+ abstract public function find(): ?CredentialEntityContract;
+
+ abstract public function forgetUser(): self;
+
+ abstract public function getCredential(): ?CredentialEntityContract;
+
+ abstract public function login(?CredentialEntityContract $credential): GuardContract;
+
+ abstract public function logout(): GuardContract;
+
+ abstract public function refreshUser(): void;
+
+ abstract public function setCredential(?CredentialEntityContract $credential = null): GuardContract;
+
+ /**
+ * Toggle the Guard's impersonation state. This should only be used by the Impersonate trait, and is not intended for use by end-users. It is public to allow for testing.
+ *
+ * @param CredentialEntityContract $credential
+ */
+ abstract public function setImpersonating(
+ CredentialEntityContract $credential,
+ ): self;
+
+ abstract public function setUser(
+ Authenticatable $user,
+ ): self;
+
+ abstract public function user(): ?Authenticatable;
+
+ /**
+ * Normalize a user model object for easier storage or comparison.
+ *
+ * @param Authenticatable $user User model object.
+ *
+ * @throws Exception If the user model object cannot be normalized.
+ *
+ * @return array Normalized user model object.
+ *
+ * @psalm-suppress TypeDoesNotContainType, UndefinedDocblockClass, UndefinedInterfaceMethod
+ *
+ * @codeCoverageIgnore
+ */
+ protected function normalizeUserArray(
+ Authenticatable $user,
+ ): array {
+ $response = null;
+ $implements = class_implements($user, true);
+ $implements = is_array($implements) ? $implements : [];
+
+ if (isset($implements[JsonSerializable::class])) {
+ /**
+ * @var JsonSerializable $user
+ */
+ $response = json_encode($user->jsonSerialize(), JSON_THROW_ON_ERROR);
+ }
+
+ if (null === $response && isset($implements[Jsonable::class])) {
+ /**
+ * @var Jsonable $user
+ */
+ $response = $user->toJson();
+ }
+
+ if (null === $response && isset($implements[Arrayable::class])) {
+ /**
+ * @var Arrayable $user
+ */
+ try {
+ $response = json_encode($user->toArray(), JSON_THROW_ON_ERROR);
+ } catch (Exception) {
+ }
+ }
+
+ // if (null === $response && (new ReflectionClass($user))->hasMethod('attributesToArray')) {
+ // try {
+ // // @phpstan-ignore-next-line
+ // $response = json_encode($user->attributesToArray(), JSON_THROW_ON_ERROR);
+ // } catch (\Exception) {
+ // }
+ // }
+
+ if (is_string($response)) {
+ try {
+ $response = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
+
+ if (is_array($response) && [] !== $response) {
+ /**
+ * @var array $response
+ */
+ return $response;
+ }
+ } catch (Exception) {
+ }
+ }
+
+ throw new GuardException(GuardExceptionContract::USER_MODEL_NORMALIZATION_FAILURE);
+ }
+}
diff --git a/src/Guards/GuardContract.php b/src/Guards/GuardContract.php
new file mode 100644
index 00000000..26030567
--- /dev/null
+++ b/src/Guards/GuardContract.php
@@ -0,0 +1,218 @@
+
+ */
+ public function processToken(
+ string $token,
+ ): ?array;
+
+ /**
+ * Query the /userinfo endpoint and update the currently authenticated user for the guard.
+ */
+ public function refreshUser(): void;
+
+ /**
+ * Get an Auth0 PHP SDK instance.
+ *
+ * @param bool $reset Optional. Whether to reset the SDK instance.
+ *
+ * @throws BindingResolutionException If the Auth0 class cannot be resolved.
+ * @throws NotFoundExceptionInterface If the Auth0 service cannot be found.
+ * @throws ContainerExceptionInterface If the Auth0 service cannot be resolved.
+ *
+ * @return Auth0Interface Auth0 PHP SDK instance.
+ */
+ public function sdk(
+ bool $reset = false,
+ ): Auth0Interface;
+
+ /**
+ * Sets the guard's currently configured credential.
+ *
+ * @param null|CredentialEntityContract $credential Optional. The credential to assign.
+ */
+ public function setCredential(?CredentialEntityContract $credential = null): self;
+
+ /**
+ * Toggle the Guard's impersonation state. This should only be used by the Impersonate trait, and is not intended for use by end-users. It is public to allow for testing.
+ *
+ * @param CredentialEntityContract $credential
+ */
+ public function setImpersonating(
+ CredentialEntityContract $credential,
+ ): self;
+
+ /**
+ * Sets the currently authenticated user for the guard. This method will replace the current user of an existing credential, if one is set, or establish a new one. If an existing credential uses a session source, the session will be updated.
+ *
+ * @param Authenticatable $user The user to set as authenticated.
+ */
+ public function setUser(
+ Authenticatable $user,
+ ): self;
+
+ /**
+ * Stop impersonating a user.
+ */
+ public function stopImpersonating(): void;
+
+ /**
+ * Returns the currently authenticated user for the guard, if available.
+ */
+ public function user(): ?Authenticatable;
+
+ /**
+ * This method is not currently implemented, but is required by Laravel's Guard contract.
+ *
+ * @param array $credentials
+ */
+ public function validate(
+ array $credentials = [],
+ ): bool;
+
+ /**
+ * This method is not currently implemented, but is required by Laravel's Guard contract.
+ */
+ public function viaRemember(): bool;
+}
diff --git a/src/Http/Controller/Stateful/Callback.php b/src/Http/Controller/Stateful/Callback.php
deleted file mode 100644
index 98213778..00000000
--- a/src/Http/Controller/Stateful/Callback.php
+++ /dev/null
@@ -1,130 +0,0 @@
-guard('auth0');
-
- // Check if the user already has a session:
- if ($guard->check()) {
- // They do; redirect to homepage.
- return redirect()->intended(config('auth0.routes.home', '/')); // @phpstan-ignore-line
- }
-
- $code = $request->query('code');
- $state = $request->query('state');
-
- if (! \is_string($code) || '' === $code) {
- $code = null;
- }
-
- if (! \is_string($state) || '' === $state) {
- $state = null;
- }
-
- /*
- * @var string|null $code
- * @var string|null $state
- */
-
- try {
- if (null !== $code && null !== $state) {
- event(new \Illuminate\Auth\Events\Attempting($guard::class, ['code' => $code, 'state' => $state], true));
-
- app(\Auth0\Laravel\Auth0::class)->getSdk()->exchange(
- code: $code,
- state: $state,
- );
- }
- } catch (\Throwable $exception) {
- $credentials = app(\Auth0\Laravel\Auth0::class)->getSdk()->getUser() ?? [];
- $credentials['code'] = $code;
- $credentials['state'] = $state;
- $credentials['error'] = ['description' => $exception->getMessage()];
-
- event(new \Illuminate\Auth\Events\Failed($guard::class, $guard->user(), $credentials));
-
- app(\Auth0\Laravel\Auth0::class)->getSdk()->clear();
-
- // Throw hookable $event to allow custom error handling scenarios.
- $event = new \Auth0\Laravel\Event\Stateful\AuthenticationFailed($exception, true);
- event($event);
-
- // If the event was not hooked by the host application, throw an exception:
- if ($event->getThrowException()) {
- throw $exception;
- }
- }
-
- if (null !== $request->query('error') && null !== $request->query('error_description')) {
- // Workaround to aid static analysis, due to the mixed formatting of the query() response:
- $error = $request->query('error', '');
- $errorDescription = $request->query('error_description', '');
- $error = \is_string($error) ? $error : '';
- $errorDescription = \is_string($errorDescription) ? $errorDescription : '';
-
- $credentials = [
- 'code' => $code,
- 'state' => $state,
- 'error' => ['error' => $error, 'description' => $errorDescription]
- ];
-
- event(new \Illuminate\Auth\Events\Failed($guard::class, $guard->user(), $credentials));
-
- // Clear the local session via the Auth0-PHP SDK:
- app(\Auth0\Laravel\Auth0::class)->getSdk()->clear();
-
- // Create a dynamic exception to report the API error response:
- $exception = \Auth0\Laravel\Exception\Stateful\CallbackException::apiException($error, $errorDescription);
-
- // Throw hookable $event to allow custom error handling scenarios:
- $event = new \Auth0\Laravel\Event\Stateful\AuthenticationFailed($exception, true);
- event($event);
-
- // If the event was not hooked by the host application, throw an exception:
- if ($event->getThrowException()) {
- throw $exception;
- }
- }
-
- // Ensure we have a valid user:
- $user = $guard->user();
-
- if (null !== $user) {
- event(new \Illuminate\Auth\Events\Validated($guard::class, $user));
-
- $request->session()->regenerate();
-
- // Throw hookable event to allow custom application logic for successful logins:
- $event = new \Auth0\Laravel\Event\Stateful\AuthenticationSucceeded($user);
- event($event);
- $user = $event->getUser();
-
- // Apply any mutations to the user object:
- $guard->setUser($user);
-
- event(new \Illuminate\Auth\Events\Login($guard::class, $user, true));
- event(new \Illuminate\Auth\Events\Authenticated($guard::class, $user));
- }
-
- return redirect()->intended(config('auth0.routes.home', '/')); // @phpstan-ignore-line
- }
-}
diff --git a/src/Http/Controller/Stateful/Login.php b/src/Http/Controller/Stateful/Login.php
deleted file mode 100644
index 2b8fca3f..00000000
--- a/src/Http/Controller/Stateful/Login.php
+++ /dev/null
@@ -1,34 +0,0 @@
-guard('auth0');
-
- /**
- * @var Guard $guard
- */
- if ($guard->check()) {
- return redirect()->intended(config('auth0.routes.home', '/')); // @phpstan-ignore-line
- }
-
- return redirect()->away(app(\Auth0\Laravel\Auth0::class)->getSdk()->login());
- }
-}
diff --git a/src/Http/Controller/Stateful/Logout.php b/src/Http/Controller/Stateful/Logout.php
deleted file mode 100644
index 0b1eccf2..00000000
--- a/src/Http/Controller/Stateful/Logout.php
+++ /dev/null
@@ -1,38 +0,0 @@
-guard('auth0');
-
- /**
- * @var Guard $guard
- */
- if ($guard->check()) {
- $request->session()->invalidate();
-
- $guard->logout();
-
- return redirect()->away(
- app(\Auth0\Laravel\Auth0::class)->getSdk()->authentication()->getLogoutLink(url(config('auth0.routes.home', '/'))), // @phpstan-ignore-line
- );
- }
-
- return redirect()->intended(config('auth0.routes.home', '/')); // @phpstan-ignore-line
- }
-}
diff --git a/src/Http/Middleware/Stateful/Authenticate.php b/src/Http/Middleware/Stateful/Authenticate.php
deleted file mode 100644
index e8901985..00000000
--- a/src/Http/Middleware/Stateful/Authenticate.php
+++ /dev/null
@@ -1,41 +0,0 @@
-guard('auth0');
-
- /**
- * @var Guard $guard
- */
- $user = $guard->user();
-
- if (null !== $user && $user instanceof \Auth0\Laravel\Contract\Model\Stateful\User) {
- $guard->login($user);
-
- return $next($request);
- }
-
- return redirect(config('auth0.routes.login', 'login')); // @phpstan-ignore-line
- }
-}
diff --git a/src/Http/Middleware/Stateful/AuthenticateOptional.php b/src/Http/Middleware/Stateful/AuthenticateOptional.php
deleted file mode 100644
index f34cd340..00000000
--- a/src/Http/Middleware/Stateful/AuthenticateOptional.php
+++ /dev/null
@@ -1,39 +0,0 @@
-guard('auth0');
-
- /**
- * @var Guard $guard
- */
- $user = $guard->user();
-
- if (null !== $user && $user instanceof \Auth0\Laravel\Contract\Model\Stateful\User) {
- $guard->login($user);
- }
-
- return $next($request);
- }
-}
diff --git a/src/Http/Middleware/Stateless/Authorize.php b/src/Http/Middleware/Stateless/Authorize.php
deleted file mode 100644
index 5a0121a5..00000000
--- a/src/Http/Middleware/Stateless/Authorize.php
+++ /dev/null
@@ -1,44 +0,0 @@
-guard('auth0');
-
- /**
- * @var Guard $guard
- */
- $user = $guard->user();
-
- if (null !== $user && $user instanceof \Auth0\Laravel\Contract\Model\Stateless\User) {
- if ('' !== $scope && ! $guard->hasScope($scope)) {
- abort(403, 'Forbidden');
- }
-
- $guard->login($user);
-
- return $next($request);
- }
-
- abort(401, 'Unauthorized');
- }
-}
diff --git a/src/Http/Middleware/Stateless/AuthorizeOptional.php b/src/Http/Middleware/Stateless/AuthorizeOptional.php
deleted file mode 100644
index 7ae90280..00000000
--- a/src/Http/Middleware/Stateless/AuthorizeOptional.php
+++ /dev/null
@@ -1,37 +0,0 @@
-guard('auth0');
-
- /**
- * @var Guard $guard
- */
- $user = $guard->user();
-
- if (null !== $user && $user instanceof \Auth0\Laravel\Contract\Model\Stateless\User) {
- $guard->login($user);
- }
-
- return $next($request);
- }
-}
diff --git a/src/Middleware/AuthenticateMiddleware.php b/src/Middleware/AuthenticateMiddleware.php
new file mode 100644
index 00000000..4d81dab8
--- /dev/null
+++ b/src/Middleware/AuthenticateMiddleware.php
@@ -0,0 +1,14 @@
+guard();
+ $scope = trim($scope);
+
+ if (! $guard instanceof GuardContract) {
+ abort(Response::HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error');
+ }
+
+ Events::dispatch(new StatefulMiddlewareRequest($request, $guard));
+
+ $credential = $guard->find(GuardContract::SOURCE_SESSION);
+
+ if ($credential instanceof CredentialEntityContract) {
+ if ('' === $scope || $guard->hasScope($scope, $credential)) {
+ $guard->login($credential);
+
+ return $next($request);
+ }
+
+ abort(Response::HTTP_FORBIDDEN, 'Forbidden');
+ }
+
+ return redirect()
+ ->setIntendedUrl($request->fullUrl())
+ ->to('/login'); // @phpstan-ignore-line
+ }
+}
diff --git a/src/Middleware/AuthenticateMiddlewareContract.php b/src/Middleware/AuthenticateMiddlewareContract.php
new file mode 100644
index 00000000..c821d0cb
--- /dev/null
+++ b/src/Middleware/AuthenticateMiddlewareContract.php
@@ -0,0 +1,30 @@
+guard();
+
+ if (! $guard instanceof GuardContract) {
+ abort(Response::HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error');
+ }
+
+ Events::dispatch(new StatefulMiddlewareRequest($request, $guard));
+
+ $credential = $guard->find(GuardContract::SOURCE_SESSION);
+
+ if ($credential instanceof CredentialEntityContract && ('' === $scope || $guard->hasScope($scope, $credential))) {
+ $guard->login($credential);
+ }
+
+ return $next($request);
+ }
+}
diff --git a/src/Middleware/AuthenticateOptionalMiddlewareContract.php b/src/Middleware/AuthenticateOptionalMiddlewareContract.php
new file mode 100644
index 00000000..aaed8377
--- /dev/null
+++ b/src/Middleware/AuthenticateOptionalMiddlewareContract.php
@@ -0,0 +1,30 @@
+shouldUse('auth0-session');
+
+ return $next($request);
+ }
+}
diff --git a/src/Middleware/AuthenticatorMiddlewareContract.php b/src/Middleware/AuthenticatorMiddlewareContract.php
new file mode 100644
index 00000000..5ddb10a0
--- /dev/null
+++ b/src/Middleware/AuthenticatorMiddlewareContract.php
@@ -0,0 +1,12 @@
+guard();
+
+ if (! $guard instanceof GuardContract) {
+ return $next($request);
+ }
+
+ Events::dispatch(new StatelessMiddlewareRequest($request, $guard));
+
+ $credential = $guard->find(GuardContract::SOURCE_TOKEN);
+
+ if ($credential instanceof CredentialEntityContract) {
+ if ('' === $scope || $guard->hasScope($scope, $credential)) {
+ $guard->login($credential);
+
+ return $next($request);
+ }
+
+ abort(Response::HTTP_FORBIDDEN, 'Forbidden');
+ }
+
+ abort(Response::HTTP_UNAUTHORIZED, 'Unauthorized');
+ }
+}
diff --git a/src/Middleware/AuthorizeMiddlewareContract.php b/src/Middleware/AuthorizeMiddlewareContract.php
new file mode 100644
index 00000000..30f26e51
--- /dev/null
+++ b/src/Middleware/AuthorizeMiddlewareContract.php
@@ -0,0 +1,30 @@
+guard();
+
+ if (! $guard instanceof GuardContract) {
+ return $next($request);
+ }
+
+ Events::dispatch(new StatelessMiddlewareRequest($request, $guard));
+
+ $credential = $guard->find(GuardContract::SOURCE_TOKEN);
+
+ if ($credential instanceof CredentialEntityContract && ('' === $scope || $guard->hasScope($scope, $credential))) {
+ $guard->login($credential);
+
+ return $next($request);
+ }
+
+ return $next($request);
+ }
+}
diff --git a/src/Middleware/AuthorizeOptionalMiddlewareContract.php b/src/Middleware/AuthorizeOptionalMiddlewareContract.php
new file mode 100644
index 00000000..ee62c7a3
--- /dev/null
+++ b/src/Middleware/AuthorizeOptionalMiddlewareContract.php
@@ -0,0 +1,30 @@
+shouldUse('auth0-api');
+
+ return $next($request);
+ }
+}
diff --git a/src/Middleware/AuthorizerMiddlewareContract.php b/src/Middleware/AuthorizerMiddlewareContract.php
new file mode 100644
index 00000000..1fe11534
--- /dev/null
+++ b/src/Middleware/AuthorizerMiddlewareContract.php
@@ -0,0 +1,12 @@
+defaultGuard = $guard;
+ }
+ }
+
+ final public function handle(
+ Request $request,
+ Closure $next,
+ ?string $guard = null,
+ ): Response {
+ $guard = trim($guard ?? '');
+
+ if ('' === $guard) {
+ $guard = $this->defaultGuard;
+ }
+
+ auth()->shouldUse($guard);
+
+ return $next($request);
+ }
+}
diff --git a/src/Middleware/GuardMiddlewareContract.php b/src/Middleware/GuardMiddlewareContract.php
new file mode 100644
index 00000000..b903a45e
--- /dev/null
+++ b/src/Middleware/GuardMiddlewareContract.php
@@ -0,0 +1,12 @@
+fill($attributes);
- }
-
- /**
- * {@inheritdoc}
- */
- public function __get(string $key)
- {
- return $this->getAttribute($key);
- }
-
- /**
- * {@inheritdoc}
- */
- public function __set(string $key, $value): void
- {
- $this->setAttribute($key, $value);
- }
-
- /**
- * {@inheritdoc}
- */
- final public function fill(array $attributes): self
- {
- foreach ($attributes as $key => $value) {
- $this->setAttribute($key, $value);
- }
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- final public function setAttribute(string $key, $value): self
- {
- $this->attributes[$key] = $value;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- final public function getAttribute(string $key, $default = null)
- {
- return $this->attributes[$key] ?? $default;
- }
-
- /**
- * {@inheritdoc}
- */
- final public function getAuthIdentifier()
- {
- return $this->attributes['sub'] ?? $this->attributes['user_id'] ?? $this->attributes['email'] ?? null;
- }
-
- /**
- * {@inheritdoc}
- */
- final public function getAuthIdentifierName()
- {
- return 'id';
- }
-
- /**
- * {@inheritdoc}
- */
- final public function getAuthPassword(): string
- {
- return '';
- }
-
- /**
- * {@inheritdoc}
- */
- final public function getRememberToken(): string
- {
- return '';
- }
-
- /**
- * {@inheritdoc}
- *
- * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
- */
- final public function setRememberToken($value): void
- {
- }
-
- /**
- * {@inheritdoc}
- */
- final public function getRememberTokenName(): string
- {
- return '';
- }
-}
diff --git a/src/Service.php b/src/Service.php
new file mode 100644
index 00000000..be1558d4
--- /dev/null
+++ b/src/Service.php
@@ -0,0 +1,17 @@
+
+ */
+ final public static function json(ResponseInterface $response): ?array
+ {
+ if (! in_array($response->getStatusCode(), [200, 201], true)) {
+ return null;
+ }
+
+ $json = json_decode((string) $response->getBody(), true);
+
+ if (! is_array($json)) {
+ return null;
+ }
+
+ return $json;
+ }
+
+ /**
+ * Register the SDK's authentication routes and controllers.
+ *
+ * @param string $authenticationGuard The name of the authentication guard to use.
+ */
+ final public static function routes(
+ string $authenticationGuard = 'auth0-session',
+ ): void {
+ Route::group(['middleware' => ['web', 'guard:' . $authenticationGuard]], static function (): void {
+ Route::get(Configuration::string(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_LOGIN) ?? '/login', LoginController::class)->name('login');
+ Route::get(Configuration::string(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_LOGOUT) ?? '/logout', LogoutController::class)->name('logout');
+ Route::get(Configuration::string(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_CALLBACK) ?? '/callback', CallbackController::class)->name('callback');
+ });
+ }
+}
diff --git a/src/ServiceContract.php b/src/ServiceContract.php
new file mode 100644
index 00000000..fa87aa0a
--- /dev/null
+++ b/src/ServiceContract.php
@@ -0,0 +1,11 @@
+mergeConfigFrom(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'config', 'auth0.php']), 'auth0');
-
- app()->singleton(Auth0::class, static fn (): Auth0 => new Auth0());
- app()->singleton(StateInstance::class, static fn (): StateInstance => new StateInstance());
- app()->singleton(Repository::class, static fn (): Repository => new Repository());
- app()->singleton(Guard::class, static fn (): Guard => new Guard());
- app()->singleton(Provider::class, static fn (): Provider => new Provider());
- app()->singleton(Authenticate::class, static fn (): Authenticate => new Authenticate());
- app()->singleton(AuthenticateOptional::class, static fn (): AuthenticateOptional => new AuthenticateOptional());
- app()->singleton(Authorize::class, static fn (): Authorize => new Authorize());
- app()->singleton(AuthorizeOptional::class, static fn (): AuthorizeOptional => new AuthorizeOptional());
- app()->singleton(Login::class, static fn (): Login => new Login());
- app()->singleton(Logout::class, static fn (): Logout => new Logout());
- app()->singleton(Callback::class, static fn (): Callback => new Callback());
-
- app()->singleton('auth0', static fn (): Auth0 => app(Auth0::class));
-
- app()->terminating(static function (): void {
- app()->instance(StateInstance::class, null);
- });
-
- return $this;
- }
-
- public function boot(\Illuminate\Routing\Router $router, \Illuminate\Auth\AuthManager $auth): self
- {
- $this->publishes([implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'config', 'auth0.php']) => config_path('auth0.php')], 'auth0-config');
-
- $auth->extend('auth0', static fn (): Guard => new Guard());
- $auth->provider('auth0', static fn (): Provider => new Provider());
-
- $router->aliasMiddleware('auth0.authenticate', Authenticate::class);
- $router->aliasMiddleware('auth0.authenticate.optional', AuthenticateOptional::class);
- $router->aliasMiddleware('auth0.authorize', Authorize::class);
- $router->aliasMiddleware('auth0.authorize.optional', AuthorizeOptional::class);
-
- return $this;
- }
}
diff --git a/src/ServiceProviderAbstract.php b/src/ServiceProviderAbstract.php
new file mode 100644
index 00000000..51770e40
--- /dev/null
+++ b/src/ServiceProviderAbstract.php
@@ -0,0 +1,229 @@
+mergeConfigFrom(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'config', 'auth0.php']), 'auth0');
+ $this->publishes([implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'config', 'auth0.php']) => config_path('auth0.php')], 'auth0');
+
+ $auth->extend('auth0.authenticator', static fn (Application $app, string $name, array $config): AuthenticationGuard => new AuthenticationGuard($name, $config));
+ $auth->extend('auth0.authorizer', static fn (Application $app, string $name, array $config): AuthorizationGuard => new AuthorizationGuard($name, $config));
+ $auth->provider('auth0.provider', static fn (Application $app, array $config): UserProvider => new UserProvider($config));
+
+ $router->aliasMiddleware('guard', GuardMiddleware::class);
+
+ $gate->define('scope', static function (Authenticatable $user, string $scope, ?GuardContract $guard = null): bool {
+ $guard ??= auth()->guard();
+
+ if (! $guard instanceof GuardContract) {
+ return false;
+ }
+
+ return $guard->hasScope($scope);
+ });
+
+ $gate->define('permission', static function (Authenticatable $user, string $permission, ?GuardContract $guard = null): bool {
+ $guard ??= auth()->guard();
+
+ if (! $guard instanceof GuardContract) {
+ return false;
+ }
+
+ return $guard->hasPermission($permission);
+ });
+
+ $gate->before(static function (?Authenticatable $user, ?string $ability) {
+ $guard = auth()->guard();
+
+ if (! $guard instanceof GuardContract || ! $user instanceof Authenticatable || ! is_string($ability)) {
+ return;
+ }
+
+ if (str_starts_with($ability, 'scope:')) {
+ if ($guard->hasScope(substr($ability, 6))) {
+ return Response::allow();
+ }
+
+ return Response::deny();
+ }
+
+ if (str_contains($ability, ':')) {
+ if ($guard->hasPermission($ability)) {
+ return Response::allow();
+ }
+
+ return Response::deny();
+ }
+ });
+
+ $this->registerDeprecated($router, $auth);
+ $this->registerMiddleware($router);
+ $this->registerRoutes();
+
+ return $this;
+ }
+
+ final public function provides()
+ {
+ return [
+ Auth0::class,
+ AuthenticateMiddleware::class,
+ AuthenticateOptionalMiddleware::class,
+ AuthenticationGuard::class,
+ AuthenticatorMiddleware::class,
+ AuthorizationGuard::class,
+ AuthorizeMiddleware::class,
+ AuthorizeOptionalMiddleware::class,
+ AuthorizerMiddleware::class,
+ CacheBridge::class,
+ CacheItemBridge::class,
+ CallbackController::class,
+ Configuration::class,
+ Guard::class,
+ GuardMiddleware::class,
+ LoginController::class,
+ LogoutController::class,
+ Service::class,
+ SessionBridge::class,
+ UserProvider::class,
+ UserRepository::class,
+ ];
+ }
+
+ final public function register(): self
+ {
+ $this->registerGuards();
+
+ $this->app->singleton(Auth0::class, static fn (): Service => new Service());
+ $this->app->singleton(Service::class, static fn (): Service => new Service());
+ $this->app->singleton(Configuration::class, static fn (): Configuration => new Configuration());
+ $this->app->singleton(Service::class, static fn (): Service => new Service());
+ $this->app->singleton(AuthenticatorMiddleware::class, static fn (): AuthenticatorMiddleware => new AuthenticatorMiddleware());
+ $this->app->singleton(AuthorizerMiddleware::class, static fn (): AuthorizerMiddleware => new AuthorizerMiddleware());
+ $this->app->singleton(AuthenticateMiddleware::class, static fn (): AuthenticateMiddleware => new AuthenticateMiddleware());
+ $this->app->singleton(AuthenticateOptionalMiddleware::class, static fn (): AuthenticateOptionalMiddleware => new AuthenticateOptionalMiddleware());
+ $this->app->singleton(AuthorizeMiddleware::class, static fn (): AuthorizeMiddleware => new AuthorizeMiddleware());
+ $this->app->singleton(AuthorizeOptionalMiddleware::class, static fn (): AuthorizeOptionalMiddleware => new AuthorizeOptionalMiddleware());
+ $this->app->singleton(GuardMiddleware::class, static fn (): GuardMiddleware => new GuardMiddleware());
+ $this->app->singleton(CallbackController::class, static fn (): CallbackController => new CallbackController());
+ $this->app->singleton(LoginController::class, static fn (): LoginController => new LoginController());
+ $this->app->singleton(LogoutController::class, static fn (): LogoutController => new LogoutController());
+ $this->app->singleton(UserProvider::class, static fn (): UserProvider => new UserProvider());
+ $this->app->singleton(UserRepository::class, static fn (): UserRepository => new UserRepository());
+
+ $this->app->singleton('auth0', static fn (): Service => app(Service::class));
+ $this->app->singleton('auth0.repository', static fn (): UserRepository => app(UserRepository::class));
+
+ return $this;
+ }
+
+ final public function registerDeprecated(
+ Router $router,
+ AuthManager $auth,
+ ): void {
+ $auth->extend('auth0.guard', static fn (Application $app, string $name, array $config): Guard => new Guard($name, $config));
+
+ $router->aliasMiddleware('auth0.authenticate.optional', AuthenticateOptionalMiddleware::class);
+ $router->aliasMiddleware('auth0.authenticate', AuthenticateMiddleware::class);
+ $router->aliasMiddleware('auth0.authorize.optional', AuthorizeOptionalMiddleware::class);
+ $router->aliasMiddleware('auth0.authorize', AuthorizeMiddleware::class);
+ }
+
+ /**
+ * @codeCoverageIgnore
+ */
+ final public function registerGuards(): void
+ {
+ if (true === config('auth0.registerGuards')) {
+ if (null === config('auth.guards.auth0-session')) {
+ config([
+ 'auth.guards.auth0-session' => [
+ 'driver' => 'auth0.authenticator',
+ 'configuration' => 'web',
+ 'provider' => 'auth0-provider',
+ ],
+ ]);
+ }
+
+ if (null === config('auth.guards.auth0-api')) {
+ config([
+ 'auth.guards.auth0-api' => [
+ 'driver' => 'auth0.authorizer',
+ 'configuration' => 'api',
+ 'provider' => 'auth0-provider',
+ ],
+ ]);
+ }
+
+ if (null === config('auth.providers.auth0-provider')) {
+ config([
+ 'auth.providers.auth0-provider' => [
+ 'driver' => 'auth0.provider',
+ 'repository' => 'auth0.repository',
+ ],
+ ]);
+ }
+ }
+ }
+
+ /**
+ * @codeCoverageIgnore
+ *
+ * @param Router $router
+ */
+ final public function registerMiddleware(
+ Router $router,
+ ): void {
+ if (true === config('auth0.registerMiddleware')) {
+ $kernel = $this->app->make(Kernel::class);
+
+ /**
+ * @var \Illuminate\Foundation\Http\Kernel $kernel
+ */
+ $kernel->appendMiddlewareToGroup('web', AuthenticatorMiddleware::class);
+ $kernel->prependToMiddlewarePriority(AuthenticatorMiddleware::class);
+
+ $kernel->appendMiddlewareToGroup('api', AuthorizerMiddleware::class);
+ $kernel->prependToMiddlewarePriority(AuthorizerMiddleware::class);
+ }
+ }
+
+ final public function registerRoutes(): void
+ {
+ if (true === config('auth0.registerAuthenticationRoutes')) {
+ Route::group(['middleware' => 'web'], static function (): void {
+ Route::get(Configuration::string(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_LOGIN) ?? '/login', LoginController::class)->name('login');
+ Route::get(Configuration::string(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_LOGOUT) ?? '/logout', LogoutController::class)->name('logout');
+ Route::get(Configuration::string(Configuration::CONFIG_NAMESPACE_ROUTES . Configuration::CONFIG_ROUTE_CALLBACK) ?? '/callback', CallbackController::class)->name('callback');
+ });
+ }
+ }
+}
diff --git a/src/ServiceProviderContract.php b/src/ServiceProviderContract.php
new file mode 100644
index 00000000..16cc09c2
--- /dev/null
+++ b/src/ServiceProviderContract.php
@@ -0,0 +1,44 @@
+user = null;
- $this->decoded = null;
- $this->idToken = null;
- $this->accessToken = null;
- $this->accessTokenScope = null;
- $this->accessTokenExpiration = null;
- $this->refreshToken = null;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getUser(): ?Authenticatable
- {
- return $this->user;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setUser(?Authenticatable $user): self
- {
- $this->user = $user;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getDecoded(): ?array
- {
- return $this->decoded;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setDecoded(?array $data): self
- {
- $this->decoded = $data;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getIdToken(): ?string
- {
- return $this->idToken;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setIdToken(?string $idToken): self
- {
- $this->idToken = $idToken;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getAccessToken(): ?string
- {
- return $this->accessToken;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setAccessToken(?string $accessToken): self
- {
- $this->accessToken = $accessToken;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getAccessTokenScope(): ?array
- {
- return $this->accessTokenScope;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setAccessTokenScope(?array $accessTokenScope): self
- {
- $this->accessTokenScope = $accessTokenScope;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getAccessTokenExpiration(): ?int
- {
- return $this->accessTokenExpiration;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setAccessTokenExpiration(?int $accessTokenExpiration): self
- {
- $this->accessTokenExpiration = $accessTokenExpiration;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getAccessTokenExpired(): ?bool
- {
- $expires = $this->getAccessTokenExpiration();
-
- if (null === $expires) {
- return null;
- }
-
- return time() >= $expires;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getRefreshToken(): ?string
- {
- return $this->refreshToken;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setRefreshToken(?string $refreshToken): self
- {
- $this->refreshToken = $refreshToken;
-
- return $this;
- }
-}
diff --git a/src/Store/LaravelSession.php b/src/Store/LaravelSession.php
deleted file mode 100644
index 793385f6..00000000
--- a/src/Store/LaravelSession.php
+++ /dev/null
@@ -1,128 +0,0 @@
-boot();
- $this->getStore()->
- put($this->getPrefixedKey($key), $value);
- }
-
- /**
- * Dispatch event to retrieve the value of a key-value pair.
- *
- * @param string $key session key to query
- * @param mixed $default default to return if nothing was found
- * @return mixed
- */
- public function get(string $key, $default = null)
- {
- $this->boot();
-
- return $this->getStore()->
- get($this->getPrefixedKey($key), $default);
- }
-
- /**
- * Dispatch event to clear all key-value pairs.
- */
- public function purge(): void
- {
- $this->boot();
-
- // It would be unwise for us to simply flush() a session here, as it is shared with the app ecosystem.
- // Instead, iterate through the session data, and if they key is prefixed with our assigned string, delete it.
-
- $pairs = $this->getStore()->
- all();
- $prefix = $this->prefix . '_';
-
- foreach (array_keys($pairs) as $key) {
- if (mb_substr($key, 0, mb_strlen($prefix)) === $prefix) {
- $this->delete($key);
- }
- }
- }
-
- /**
- * Dispatch event to delete key-value pair.
- *
- * @param string $key session key to delete
- */
- public function delete(string $key): void
- {
- $this->boot();
- $this->getStore()->
- forget($this->getPrefixedKey($key));
- }
-
- /**
- * Dispatch event to alert that a session should be prepared for an incoming request.
- */
- private function boot(): void
- {
- if (! $this->booted) {
- if (! $this->getStore()->isStarted()) {
- $this->getStore()->
- start();
- }
-
- $this->booted = true;
- }
- }
-
- private function getStore(): \Illuminate\Session\Store
- {
- $request = request();
-
- // @phpstan-ignore-next-line
- if ($request instanceof \Illuminate\Http\Request) {
- return $request->session();
- }
-
- // @phpstan-ignore-next-line
- throw new Exception('A cache must be configured.');
- }
-
- private function getPrefixedKey(string $key): string
- {
- if ('' !== $this->prefix) {
- return $this->prefix . '_' . $key;
- }
-
- return $key;
- }
-}
diff --git a/src/Traits/ActingAsAuth0User.php b/src/Traits/ActingAsAuth0User.php
index 10a3b880..fbeb7d3e 100644
--- a/src/Traits/ActingAsAuth0User.php
+++ b/src/Traits/ActingAsAuth0User.php
@@ -4,36 +4,71 @@
namespace Auth0\Laravel\Traits;
-use Auth0\Laravel\Model\Stateless\User;
-use Auth0\Laravel\StateInstance;
-use Illuminate\Contracts\Auth\Authenticatable as UserContract;
+use Auth0\Laravel\Entities\CredentialEntity;
+use Auth0\Laravel\Guards\GuardContract;
+use Auth0\Laravel\UserProvider;
+use Auth0\Laravel\Users\ImposterUser;
+use Illuminate\Contracts\Auth\Authenticatable;
+/**
+ * Set the currently logged in user for the application. Only intended for unit testing.
+ *
+ * @deprecated 7.8.0 Use Auth0\Laravel\Traits\Impersonate instead.
+ *
+ * @api
+ */
trait ActingAsAuth0User
{
- abstract public function actingAs(UserContract $user, $guard = null);
+ public array $defaultActingAsAttributes = [
+ 'sub' => 'some-auth0-user-id',
+ 'azp' => 'some-auth0-application-client-id',
+ 'scope' => '',
+ ];
/**
- * use this method to impersonate a specific auth0 user
- * if you pass an attributes array, it will be merged with a set of default values.
+ * Set the currently logged in user for the application. Only intended for unit testing.
*
- * @return mixed
+ * @param array $attributes The attributes to use for the user.
+ * @param null|string $guard The guard to impersonate with.
+ * @param ?int $source
*/
- public function actingAsAuth0User(array $attributes = [])
- {
- $defaults = [
- 'sub' => 'some-auth0-user-id',
- 'azp' => 'some-auth0-appplication-client-id',
- 'iat' => time(),
- 'exp' => time() + 60 * 60,
- 'scope' => '',
- ];
-
- $auth0user = new User(array_merge($defaults, $attributes));
-
- if ($auth0user->getAttribute('scope')) {
- app(StateInstance::class)->setAccessTokenScope(explode(' ', $auth0user->getAttribute('scope')));
+ public function actingAsAuth0User(
+ array $attributes = [],
+ ?string $guard = null,
+ ?int $source = GuardContract::SOURCE_TOKEN,
+ ): self {
+ $issued = time();
+ $expires = $issued + 60 * 60;
+ $timestamps = ['iat' => $issued, 'exp' => $expires];
+ $attributes = array_merge($this->defaultActingAsAttributes, $timestamps, $attributes);
+ $scope = $attributes['scope'] ? explode(' ', $attributes['scope']) : [];
+ unset($attributes['scope']);
+
+ $instance = auth()->guard($guard);
+
+ if (! $instance instanceof GuardContract) {
+ $user = new ImposterUser($attributes);
+
+ return $this->actingAs($user, $guard);
}
- return $this->actingAs($auth0user, 'auth0');
+ $provider = new UserProvider();
+
+ if (GuardContract::SOURCE_SESSION === $source) {
+ $user = $provider->getRepository()->fromSession($attributes);
+ } else {
+ $user = $provider->getRepository()->fromAccessToken($attributes);
+ }
+
+ $credential = CredentialEntity::create(
+ user: $user,
+ accessTokenScope: $scope,
+ );
+
+ $instance->setImpersonating($credential, $source);
+
+ return $this->actingAs($user, $guard);
}
+
+ abstract public function actingAs(Authenticatable $user, $guard = null);
}
diff --git a/src/Traits/Impersonate.php b/src/Traits/Impersonate.php
new file mode 100644
index 00000000..1539c879
--- /dev/null
+++ b/src/Traits/Impersonate.php
@@ -0,0 +1,89 @@
+impersonateSession($credential, $guard);
+ }
+
+ if (GuardContract::SOURCE_TOKEN === $source || null === $source) {
+ $this->impersonateToken($credential, $guard);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Pretend to be an authenticated user for the request. Only intended for unit testing.
+ *
+ * @param CredentialEntityContract $credential The Credential to impersonate.
+ * @param null|string $guard The guard to impersonate with.
+ *
+ * @return $this The current test case instance.
+ */
+ public function impersonateSession(
+ CredentialEntityContract $credential,
+ ?string $guard = null,
+ ): self {
+ $instance = auth()->guard($guard);
+ $user = $credential->getUser() ?? new ImposterUser([]);
+
+ if ($instance instanceof GuardContract) {
+ $instance->setImpersonating($credential, GuardContract::SOURCE_SESSION);
+ }
+
+ return $this->actingAs($user, $guard);
+ }
+
+ /**
+ * Pretend to be a bearer token-established stateless user for the request. Only intended for unit testing.
+ *
+ * @param CredentialEntityContract $credential The Credential to impersonate.
+ * @param null|string $guard The guard to impersonate with.
+ *
+ * @return $this The current test case instance.
+ */
+ public function impersonateToken(
+ CredentialEntityContract $credential,
+ ?string $guard = null,
+ ): self {
+ $instance = auth()->guard($guard);
+ $user = $credential->getUser() ?? new ImposterUser([]);
+
+ if ($instance instanceof GuardContract) {
+ $instance->setImpersonating($credential, GuardContract::SOURCE_TOKEN);
+ }
+
+ return $this->actingAs($user, $guard);
+ }
+
+ abstract public function actingAs(Authenticatable $user, $guard = null);
+}
diff --git a/src/UserProvider.php b/src/UserProvider.php
new file mode 100644
index 00000000..5f466b7e
--- /dev/null
+++ b/src/UserProvider.php
@@ -0,0 +1,16 @@
+repository ?? $this->resolveRepository();
+ }
+
+ /**
+ * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
+ *
+ * @param Authenticatable $user
+ * @param array $credentials
+ * @param bool $force
+ *
+ * @codeCoverageIgnore
+ */
+ final public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void
+ {
+ }
+
+ /**
+ * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
+ *
+ * @param array $credentials
+ */
+ final public function retrieveByCredentials(array $credentials): ?Authenticatable
+ {
+ if ([] === $credentials) {
+ return null;
+ }
+
+ $hash = hash('sha256', json_encode($credentials, JSON_THROW_ON_ERROR) ?: ''); /** @phpstan-ignore-line */
+ $cached = $this->withoutRecording(static fn (): mixed => Cache::get('auth0_sdk_credential_lookup_' . $hash));
+
+ if ($cached instanceof Authenticatable) {
+ return $cached;
+ }
+
+ static $lastResponse = null;
+ static $lastCredentials = null;
+
+ // @codeCoverageIgnoreStart
+
+ /**
+ * @var ?Authenticatable $lastResponse
+ * @var array $lastCredentials
+ */
+ if ($lastCredentials === $credentials) {
+ return $lastResponse;
+ }
+
+ if (class_exists(self::TELESCOPE) && true === config('telescope.enabled')) {
+ static $depth = 0;
+ static $lastCalled = null;
+
+ /**
+ * @var int $depth
+ * @var ?int $lastCalled
+ */
+ if (null === $lastCalled) {
+ $lastCalled = time();
+ }
+
+ if ($lastCredentials !== $credentials || time() - $lastCalled > 10) {
+ $lastResponse = null;
+ $depth = 0;
+ }
+
+ if ($depth >= 1) {
+ return $lastResponse;
+ }
+
+ ++$depth;
+ $lastCalled = time();
+ $lastCredentials = $credentials;
+ }
+
+ // @codeCoverageIgnoreEnd
+
+ $lastResponse = $this->getRepository()->fromSession($credentials);
+
+ $this->withoutRecording(static fn (): bool => Cache::put('auth0_sdk_credential_lookup_' . $hash, $lastResponse, 5));
+
+ return $lastResponse;
+ }
+
+ /**
+ * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
+ *
+ * @codeCoverageIgnore
+ *
+ * @param mixed $identifier
+ */
+ final public function retrieveById($identifier): ?Authenticatable
+ {
+ return null;
+ }
+
+ /**
+ * @psalm-suppress DocblockTypeContradiction
+ *
+ * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
+ *
+ * @param mixed $identifier
+ * @param mixed $token
+ */
+ final public function retrieveByToken($identifier, $token): ?Authenticatable
+ {
+ // @phpstan-ignore-next-line
+ if (! is_string($token)) {
+ return null;
+ }
+
+ $guard = auth()->guard();
+
+ if (! $guard instanceof GuardContract) {
+ return null;
+ }
+
+ $user = $guard->processToken($token);
+
+ return null !== $user ? $this->getRepository()->fromAccessToken($user) : null;
+ }
+
+ final public function setRepository(string $repository): void
+ {
+ $this->resolveRepository($repository);
+ }
+
+ /**
+ * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
+ *
+ * @codeCoverageIgnore
+ *
+ * @param Authenticatable $user
+ * @param mixed $token
+ */
+ final public function updateRememberToken(Authenticatable $user, $token): void
+ {
+ }
+
+ /**
+ * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
+ *
+ * @param Authenticatable $user
+ * @param array $credentials
+ */
+ final public function validateCredentials(
+ Authenticatable $user,
+ array $credentials,
+ ): bool {
+ return false;
+ }
+
+ protected function getConfiguration(
+ string $key,
+ ): array | string | null {
+ return $this->config[$key] ?? null;
+ }
+
+ protected function getRepositoryName(): string
+ {
+ return $this->repositoryName;
+ }
+
+ protected function resolveRepository(
+ ?string $repositoryName = null,
+ ): UserRepositoryContract {
+ $model = $repositoryName;
+ $model ??= $this->getConfiguration('model');
+ $model ??= $this->getConfiguration('repository');
+ $model ??= UserRepository::class;
+
+ if ($model === $this->getRepositoryName()) {
+ return $this->getRepository();
+ }
+
+ if (! is_string($model)) {
+ throw new BindingResolutionException('The configured Repository could not be loaded.');
+ }
+
+ if (! app()->bound($model)) {
+ try {
+ app()->make($model);
+ } catch (BindingResolutionException) {
+ throw new BindingResolutionException(sprintf('The configured Repository %s could not be loaded.', $model));
+ }
+ }
+
+ $this->setRepositoryName($model);
+
+ return $this->repository = app($model);
+ }
+
+ protected function setConfiguration(
+ string $key,
+ string $value,
+ ): void {
+ $this->config[$key] = $value;
+ }
+
+ protected function setRepositoryName(string $repositoryName): void
+ {
+ $this->setConfiguration('model', $repositoryName);
+ $this->repositoryName = $repositoryName;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ *
+ * @param callable $callback
+ */
+ protected function withoutRecording(callable $callback): mixed
+ {
+ if (class_exists(self::TELESCOPE)) {
+ return self::TELESCOPE::withoutRecording($callback);
+ }
+
+ return $callback();
+ }
+}
diff --git a/src/UserProviderContract.php b/src/UserProviderContract.php
new file mode 100644
index 00000000..6025a28e
--- /dev/null
+++ b/src/UserProviderContract.php
@@ -0,0 +1,18 @@
+fill($attributes);
+ }
+
+ public function __get(string $key): mixed
+ {
+ return $this->getAttribute($key);
+ }
+
+ public function __set(string $key, mixed $value): void
+ {
+ $this->setAttribute($key, $value);
+ }
+
+ final public function getAttribute(string $key, mixed $default = null): mixed
+ {
+ return $this->attributes[$key] ?? $default;
+ }
+
+ final public function getAttributes(): mixed
+ {
+ return $this->attributes;
+ }
+
+ final public function getAuthIdentifier(): int | string | null
+ {
+ return $this->attributes['sub'] ?? $this->attributes['user_id'] ?? $this->attributes['email'] ?? null;
+ }
+
+ final public function getAuthIdentifierName(): string
+ {
+ return 'id';
+ }
+
+ final public function getAuthPassword(): string
+ {
+ return '';
+ }
+
+ final public function getAuthPasswordName(): string
+ {
+ return 'password';
+ }
+
+ final public function getRememberToken(): string
+ {
+ return '';
+ }
+
+ final public function getRememberTokenName(): string
+ {
+ return '';
+ }
+
+ final public function jsonSerialize(): mixed
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
+ *
+ * @param mixed $value
+ */
+ final public function setRememberToken(mixed $value): void
+ {
+ }
+
+ abstract public function fill(array $attributes): self;
+
+ abstract public function setAttribute(string $key, mixed $value): self;
+}
diff --git a/src/Users/UserContract.php b/src/Users/UserContract.php
new file mode 100644
index 00000000..a6270ac2
--- /dev/null
+++ b/src/Users/UserContract.php
@@ -0,0 +1,54 @@
+ $value) {
+ $this->setAttribute($key, $value);
+ }
+
+ return $this;
+ }
+
+ final public function setAttribute(string $key, mixed $value): self
+ {
+ $this->attributes[$key] = $value;
+
+ return $this;
+ }
+}
diff --git a/tests/Pest.php b/tests/Pest.php
index 06296a49..3b5a4118 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -1,39 +1,204 @@
- beforeEach(function (): void {
- $this->service = createService();
- }, )->
- in(__DIR__);
-
-function createServiceConfiguration(
- array $configuration = [],
-): Auth0\SDK\Configuration\SdkConfiguration {
- $defaults = [
- 'strategy' => 'none',
- ];
+use Auth0\Laravel\Tests\TestCase;
+use Auth0\SDK\Token;
+use Auth0\SDK\Token\Generator;
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\Cache;
+
+/*
+|--------------------------------------------------------------------------
+| Test Case
+|--------------------------------------------------------------------------
+|
+| The closure you provide to your test functions is always bound to a specific PHPUnit test
+| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
+| need to change it using the "uses()" function to bind a different classes or traits.
+|
+*/
+
+define('AUTH0_LARAVEL_RUNNING_TESTS', 1);
+
+uses(TestCase::class)->in(__DIR__);
+
+// uses()->beforeAll(function (): void {
+
+// })->in(__DIR__);
+
+uses()->beforeEach(function (): void {
+ $this->events = [];
+
+ Event::listen('*', function ($event) {
+ $this->events[] = $event;
+ });
+
+ Cache::flush();
+
+ config()->set('auth', [
+ 'defaults' => [
+ 'guard' => 'legacyGuard',
+ 'passwords' => 'users',
+ ],
+ 'guards' => [
+ 'web' => [
+ 'driver' => 'session',
+ 'provider' => 'users',
+ ],
+ 'legacyGuard' => [
+ 'driver' => 'auth0.guard',
+ 'configuration' => 'web',
+ 'provider' => 'auth0-provider',
+ ],
+ 'auth0-session' => [
+ 'driver' => 'auth0.authenticator',
+ 'configuration' => 'web',
+ 'provider' => 'auth0-provider',
+ ],
+ 'auth0-api' => [
+ 'driver' => 'auth0.authorizer',
+ 'configuration' => 'api',
+ 'provider' => 'auth0-provider',
+ ],
+ ],
+ 'providers' => [
+ 'users' => [
+ 'driver' => 'eloquent',
+ 'model' => App\Models\User::class,
+ ],
+ 'auth0-provider' => [
+ 'driver' => 'auth0.provider',
+ 'repository' => 'auth0.repository',
+ ],
+ ],
+ ]);
+})->in(__DIR__);
+
+// uses()->afterEach(function (): void {
+// $commands = ['optimize:clear'];
+
+// foreach ($commands as $command) {
+// Artisan::call($command);
+// }
+// })->in(__DIR__);
+
+uses()->compact();
+
+/*
+|--------------------------------------------------------------------------
+| Expectations
+|--------------------------------------------------------------------------
+|
+| When you're writing tests, you often need to check that values meet certain conditions. The
+| "expect()" function gives you access to a set of "expectations" methods that you can use
+| to assert different things. Of course, you may extend the Expectation API at any time.
+|
+*/
+
+// expect()->extend('toBeOne', function () {
+// return $this->toBe(1);
+// });
+
+/*
+|--------------------------------------------------------------------------
+| Functions
+|--------------------------------------------------------------------------
+|
+| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
+| project that you don't want to repeat in every file. Here you can also expose helpers as
+| global functions to help you to reduce the number of lines of code in your test files.
+|
+*/
+
+// function something()
+// {
+// // ..
+// }
- return new \Auth0\SDK\Configuration\SdkConfiguration(array_merge($defaults, $configuration));
+function mockIdToken(
+ string $algorithm = Token::ALGO_RS256,
+ array $claims = [],
+ array $headers = []
+): string {
+ $secret = createRsaKeys()->private;
+
+ $claims = array_merge([
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ 'sub' => 'hello|world',
+ 'aud' => config('auth0.guards.default.clientId'),
+ 'exp' => time() + 60,
+ 'iat' => time(),
+ 'email' => 'john.doe@somewhere.test'
+ ], $claims);
+
+ return (string) Generator::create($secret, $algorithm, $claims, $headers);
+}
+
+function mockAccessToken(
+ string $algorithm = Token::ALGO_RS256,
+ array $claims = [],
+ array $headers = []
+): string {
+ $secret = createRsaKeys()->private;
+
+ $claims = array_merge([
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ 'sub' => 'hello|world',
+ 'aud' => config('auth0.guards.default.clientId'),
+ 'iat' => time(),
+ 'exp' => time() + 60,
+ 'azp' => config('auth0.guards.default.clientId'),
+ 'scope' => 'openid profile email',
+ ], $claims);
+
+ return (string) Generator::create($secret, $algorithm, $claims, $headers);
}
-function createService(
- ?Auth0\SDK\Configuration\SdkConfiguration $configuration = null,
-): Auth0\Laravel\Auth0 {
- if (null === $configuration) {
- $configuration = createServiceConfiguration();
+function createRsaKeys(
+ string $digestAlg = 'sha256',
+ int $keyType = OPENSSL_KEYTYPE_RSA,
+ int $bitLength = 2048
+): object
+{
+ $config = [
+ 'digest_alg' => $digestAlg,
+ 'private_key_type' => $keyType,
+ 'private_key_bits' => $bitLength,
+ ];
+
+ $privateKeyResource = openssl_pkey_new($config);
+
+ if ($privateKeyResource === false) {
+ throw new RuntimeException("OpenSSL reported an error: " . getSslError());
+ }
+
+ $export = openssl_pkey_export($privateKeyResource, $privateKey);
+
+ if ($export === false) {
+ throw new RuntimeException("OpenSSL reported an error: " . getSslError());
}
- return (new \Auth0\Laravel\Auth0())->setConfiguration($configuration);
+ $publicKey = openssl_pkey_get_details($privateKeyResource);
+
+ $resCsr = openssl_csr_new([], $privateKeyResource);
+ $resCert = openssl_csr_sign($resCsr, null, $privateKeyResource, 30);
+ openssl_x509_export($resCert, $x509);
+
+ return (object) [
+ 'private' => $privateKey,
+ 'public' => $publicKey['key'],
+ 'cert' => $x509,
+ 'resource' => $privateKeyResource,
+ ];
}
-function createSdk(
- ?Auth0\SDK\Configuration\SdkConfiguration $configuration = null,
-): Auth0\SDK\Auth0 {
- if (null === $configuration) {
- $configuration = createServiceConfiguration();
+function getSslError(): string
+{
+ $errors = [];
+
+ while ($error = openssl_error_string()) {
+ $errors[] = $error;
}
- return new \Auth0\SDK\Auth0($configuration);
+ return implode(', ', $errors);
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 7df2ef4a..0ce025f0 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -4,24 +4,91 @@
namespace Auth0\Laravel\Tests;
-use Orchestra\Testbench\TestCase as Orchestra;
+use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
+use Auth0\Laravel\ServiceProvider;
+use Orchestra\Testbench\Concerns\CreatesApplication;
+use Spatie\LaravelRay\RayServiceProvider;
-class TestCase extends Orchestra
+class TestCase extends BaseTestCase
{
- protected function setUp(): void
+ use CreatesApplication;
+
+ protected $enablesPackageDiscoveries = true;
+ protected $events = [];
+
+ protected function getPackageProviders($app)
{
- parent::setUp();
+ return [
+ RayServiceProvider::class,
+ ServiceProvider::class,
+ ];
}
- public function refreshServiceProvider(): void
+ protected function getEnvironmentSetUp($app): void
{
- (new \Auth0\Laravel\ServiceProvider($this->app))->packageBooted();
+ // Set a random key for testing
+ $_ENV['APP_KEY'] = 'base64:' . base64_encode(random_bytes(32));
+
+ // Setup database for testing (currently unused)
+ $app['config']->set('database.default', 'testbench');
+ $app['config']->set('database.connections.testbench', [
+ 'driver' => 'sqlite',
+ 'database' => ':memory:',
+ 'prefix' => '',
+ ]);
}
- protected function getPackageProviders($app)
+ /**
+ * Asserts that an event was dispatched. Optionally assert the number of times it was dispatched and/or that it was dispatched after another event.
+ *
+ * @param string $expectedEvent The event to assert was dispatched.
+ * @param int $times The number of times the event was expected to be dispatched.
+ * @param string|null $followingEvent The event that was expected to be dispatched before this event.
+ */
+ protected function assertDispatched(string $expectedEvent, int $times = 0, ?string $followingEvent = null)
{
- return [
- \Auth0\Laravel\ServiceProvider::class,
- ];
+ expect($this->events)
+ ->toBeArray()
+ ->toContain($expectedEvent);
+
+ if ($times > 0) {
+ expect(array_count_values($this->events)[$expectedEvent])
+ ->toBeInt()
+ ->toBe($times);
+ }
+
+ if (null !== $followingEvent) {
+ expect($this->events)
+ ->toContain($followingEvent);
+
+ $indexExpected = array_search($expectedEvent, $this->events);
+ $indexFollowing = array_search($followingEvent, $this->events);
+
+ if ($indexExpected !== false && $indexFollowing !== false) {
+ expect($indexExpected)
+ ->toBeInt()
+ ->toBeGreaterThan($indexFollowing);
+ }
+ }
+ }
+
+ /**
+ * Asserts that events were dispatched in the order provided. Events not in the array are ignored.
+ *
+ * @param array $events Array of events to assert were dispatched in order.
+ */
+ protected function assertDispatchedOrdered(array $events)
+ {
+ $previousIndex = -1;
+
+ foreach ($events as $event) {
+ $index = array_search($event, $this->events);
+
+ expect($index)
+ ->toBeInt()
+ ->toBeGreaterThan($previousIndex);
+
+ $previousIndex = $index;
+ }
}
}
diff --git a/tests/Unit/Auth/GuardStatefulTest.php b/tests/Unit/Auth/GuardStatefulTest.php
new file mode 100644
index 00000000..857db265
--- /dev/null
+++ b/tests/Unit/Auth/GuardStatefulTest.php
@@ -0,0 +1,420 @@
+group('auth', 'auth.guard', 'auth.guard.stateful');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = $guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+ $this->config = $this->sdk->configuration();
+ $this->session = $this->config->getSessionStorage();
+
+ $this->user = new StatefulUser(['sub' => uniqid('auth0|')]);
+
+ $this->session->set('user', ['sub' => 'hello|world']);
+ $this->session->set('idToken', (string) Generator::create((createRsaKeys())->private));
+ $this->session->set('accessToken', (string) Generator::create((createRsaKeys())->private));
+ $this->session->set('accessTokenScope', [uniqid()]);
+ $this->session->set('accessTokenExpiration', time() + 60);
+
+ $this->route = '/' . uniqid();
+ $guard = $this->guard;
+
+ Route::get($this->route, function () use ($guard) {
+ $credential = $guard->find(Guard::SOURCE_SESSION);
+
+ if (null !== $credential) {
+ $guard->login($credential, Guard::SOURCE_SESSION);
+ return response()->json(['status' => 'OK']);
+ }
+
+ abort(Response::HTTP_UNAUTHORIZED, 'Unauthorized');
+ });
+});
+
+it('gets a user from a valid session', function (): void {
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe('hello|world');
+});
+
+it('updates internal and session states as appropriate', function (): void {
+ // Session should be available and populated
+ expect($this->session)
+ ->getAll()->not()->toBe([]);
+
+ // Guard should pick up on the session during the HTTP request
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ // Guard should have it's state populated
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe('hello|world');
+
+ // Empty guard state
+ $this->guard->logout();
+
+ // Guard should have had it's state emptied
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ // Session should have been emptied
+ expect($this->session)
+ ->getAll()->toBe([]);
+
+ // HTTP request should fail without a session.
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_UNAUTHORIZED);
+
+ // Inject a new session into the store
+ $this->session->set('user', ['sub' => 'hello|world|two']);
+
+ // Session should be available and populated again
+ expect($this->session)
+ ->getAll()->not()->toBe([]);
+
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ // Guard should pick up on the session
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe('hello|world|two');
+
+ // Directly wipe the Laravel session, circumventing the Guard APIs
+ $this->session->purge();
+
+ // Session should be empty
+ expect($this->session)
+ ->getAll()->toBe([]);
+
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_UNAUTHORIZED);
+
+ // Guard should have it's state emptied
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ $this->session->set('user', ['sub' => 'hello|world|4']);
+
+ // Session should be available
+ expect($this->session)
+ ->getAll()->not()->toBe([]);
+
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ // Guard should pick up on the session
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe('hello|world|4');
+
+ $identifier = uniqid('auth0|');
+ $user = new StatefulUser(['sub' => $identifier]);
+
+ // Overwrite state using the Guard's login()
+ $this->guard->login(CredentialEntity::create(
+ user: $user
+ ), Guard::SOURCE_SESSION);
+
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ // Guard should have it's state updated
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe($identifier);
+
+ // Session should be updated
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier]);
+});
+
+it('creates a session from login()', function (): void {
+ $identifier = uniqid('auth0|');
+ // $idToken = uniqid('id-token-');
+ // $accessToken = uniqid('access-token-');
+ $accessTokenScope = [uniqid('access-token-scope-')];
+ $accessTokenExpiration = time() + 60;
+
+ $this->session->set('user', ['sub' => $identifier]);
+ // $this->session->set('idToken', $idToken);
+ // $this->session->set('accessToken', $accessToken);
+ $this->session->set('accessTokenScope', $accessTokenScope);
+ $this->session->set('accessTokenExpiration', $accessTokenExpiration);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+
+ expect($found)
+ ->toBeInstanceOf(CredentialEntity::class);
+
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier])
+ // ->get('idToken')->toBe($idToken)
+ // ->get('accessToken')->toBe($accessToken)
+ ->get('accessTokenScope')->toBe($accessTokenScope)
+ ->get('accessTokenExpiration')->toBe($accessTokenExpiration)
+ ->get('refreshToken')->toBeNull();
+
+ $user = new StatefulUser(['sub' => $identifier]);
+
+ $changedIdToken = uniqid('CHANGED-id-token-');
+ $changedRefreshToken = uniqid('CHANGED-refresh-token-');
+
+ // Overwrite state using the Guard's login()
+ $this->guard->login(CredentialEntity::create(
+ user: $user,
+ idToken: $changedIdToken,
+ refreshToken: $changedRefreshToken
+ ), Guard::SOURCE_SESSION);
+
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe($identifier);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier])
+ ->get('idToken')->toBe($changedIdToken)
+ // ->get('accessToken')->toBe($accessToken)
+ ->get('accessTokenScope')->toBe($accessTokenScope)
+ ->get('accessTokenExpiration')->toBe($accessTokenExpiration)
+ ->get('refreshToken')->toBe($changedRefreshToken);
+});
+
+it('queries the /userinfo endpoint for refreshUser()', function (): void {
+ $identifier = uniqid('auth0|');
+ // $idToken = uniqid('id-token-');
+ // $accessToken = uniqid('access-token-');
+ $accessTokenScope = [uniqid('access-token-scope-')];
+ $accessTokenExpiration = time() + 60;
+
+ $this->session->set('user', ['sub' => $identifier]);
+ // $this->session->set('idToken', $idToken);
+ // $this->session->set('accessToken', $accessToken);
+ $this->session->set('accessTokenScope', $accessTokenScope);
+ $this->session->set('accessTokenExpiration', $accessTokenExpiration);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier]);
+
+ $response = (new MockResponseFactory)->createResponse();
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new MockStreamFactory)->createStream(
+ json_encode(
+ value: [
+ 'sub' => $identifier,
+ 'name' => 'John Doe',
+ 'email' => '...',
+ ],
+ flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
+ )
+ )
+ )
+ );
+
+ $this->guard->refreshUser();
+
+ $userAttributes = $this->guard->user()->getAttributes();
+
+ expect($userAttributes)
+ ->toBeArray()
+ ->toMatchArray([
+ 'sub' => $identifier,
+ 'name' => 'John Doe',
+ 'email' => '...',
+ ]);
+});
+
+it('does not query the /userinfo endpoint for refreshUser() if an access token is not available', function (): void {
+ $identifier = uniqid('auth0|');
+ // $idToken = uniqid('id-token-');
+ $accessTokenScope = [uniqid('access-token-scope-')];
+ $accessTokenExpiration = time() + 60;
+
+ $this->session->set('user', ['sub' => $identifier]);
+ // $this->session->set('idToken', $idToken);
+ $this->session->set('accessToken', null);
+ $this->session->set('accessTokenScope', $accessTokenScope);
+ $this->session->set('accessTokenExpiration', $accessTokenExpiration);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier]);
+
+ $requestFactory = new MockRequestFactory;
+ $responseFactory = new MockResponseFactory;
+ $streamFactory = new MockStreamFactory;
+
+ $response = $responseFactory->createResponse(200);
+ $response->getBody()->write(json_encode(
+ [
+ 'sub' => $identifier,
+ 'name' => 'John Doe',
+ 'email' => '...',
+ ],
+ JSON_PRETTY_PRINT
+ ));
+
+ $http = new MockHttpClient(fallbackResponse: $response, requestLimit: 0);
+ $http->addResponseWildcard($response);
+
+ $this->config->setHttpRequestFactory($requestFactory);
+ $this->config->setHttpResponseFactory($responseFactory);
+ $this->config->setHttpStreamFactory($streamFactory);
+ $this->config->setHttpClient($http);
+
+ $this->guard->refreshUser();
+
+ $userAttributes = $this->guard->user()->getAttributes();
+
+ expect($userAttributes)
+ ->toBeArray()
+ ->toMatchArray([
+ 'sub' => $identifier,
+ ]);
+});
+
+it('rejects bad responses from the /userinfo endpoint for refreshUser()', function (): void {
+ $identifier = uniqid('auth0|');
+ // $idToken = uniqid('id-token-');
+ // $accessToken = uniqid('access-token-');
+ $accessTokenScope = [uniqid('access-token-scope-')];
+ $accessTokenExpiration = time() + 60;
+
+ $this->session->set('user', ['sub' => $identifier]);
+ // $this->session->set('idToken', $idToken);
+ // $this->session->set('accessToken', $accessToken);
+ $this->session->set('accessTokenScope', $accessTokenScope);
+ $this->session->set('accessTokenExpiration', $accessTokenExpiration);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier]);
+
+ $response = (new MockResponseFactory)->createResponse();
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new MockStreamFactory)->createStream(
+ json_encode(
+ value: 'bad response',
+ flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
+ )
+ )
+ )
+ );
+
+ $this->guard->refreshUser();
+
+ $userAttributes = $this->guard->user()->getAttributes();
+
+ expect($userAttributes)
+ ->toBeArray()
+ ->toMatchArray([
+ 'sub' => $identifier,
+ ]);
+});
+
+it('immediately invalidates an expired session when a refresh token is not available', function (): void {
+ $this->session->set('accessTokenExpiration', time() - 1000);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ expect($this->session)
+ ->get('user')->toBeNull();
+});
+
+it('invalidates an expired session when an access token fails to refresh', function (): void {
+ $this->session->set('accessTokenExpiration', time() - 1000);
+ $this->session->set('refreshToken', uniqid());
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ expect($this->session)
+ ->get('user')->toBeNull();
+});
+
+it('successfully continues a session when an access token is successfully refreshed', function (): void {
+ $this->session->set('accessTokenExpiration', time() - 1000);
+ $this->session->set('refreshToken', (string) Generator::create((createRsaKeys())->private));
+
+ $response = (new MockResponseFactory)->createResponse();
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new MockStreamFactory)->createStream(
+ json_encode(
+ value: [
+ 'access_token' => (string) Generator::create((createRsaKeys())->private),
+ 'expires_in' => 60,
+ 'scope' => 'openid profile',
+ 'token_type' => 'Bearer',
+ ],
+ flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
+ )
+ )
+ )
+ );
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->guard)
+ ->user()->not()->toBeNull();
+
+ expect($this->session)
+ ->get('user')->not()->toBeNull();
+});
diff --git a/tests/Unit/Auth/GuardStatelessTest.php b/tests/Unit/Auth/GuardStatelessTest.php
new file mode 100644
index 00000000..43cccbe5
--- /dev/null
+++ b/tests/Unit/Auth/GuardStatelessTest.php
@@ -0,0 +1,96 @@
+group('auth', 'auth.guard', 'auth.guard.stateless');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_API,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.audience' => ['https://example.com/health-api'],
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+ $this->config = $this->sdk->configuration();
+
+ $this->token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ "sub" => "auth0|123456",
+ "aud" => [
+ config('auth0.guards.default.audience')[0],
+ "https://my-domain.auth0.com/userinfo"
+ ],
+ "azp" => config('auth0.guards.default.clientId'),
+ "exp" => time() + 60,
+ "iat" => time(),
+ "scope" => "openid profile read:patients read:admin"
+ ]);
+ $this->bearerToken = ['Authorization' => 'Bearer ' . $this->token->toString()];
+
+ $this->route = '/' . uniqid();
+ $guard = $this->guard;
+
+ Route::get($this->route, function () use ($guard) {
+ $credential = $guard->find(Guard::SOURCE_TOKEN);
+
+ if (null !== $credential) {
+ $guard->login($credential, Guard::SOURCE_TOKEN);
+ return response()->json(['status' => 'OK']);
+ }
+
+ abort(Response::HTTP_UNAUTHORIZED, 'Unauthorized');
+ });
+});
+
+it('assigns a user from a good token', function (): void {
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ getJson($this->route, $this->bearerToken)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($this->guard)
+ ->user()->not()->toBeNull();
+});
+
+it('does not assign a user from a empty token', function (): void {
+ getJson($this->route, ['Authorization' => 'Bearer '])
+ ->assertStatus(Response::HTTP_UNAUTHORIZED);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('does not get a user from a bad token', function (): void {
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->setAudience(['BAD_AUDIENCE']);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ getJson($this->route, $this->bearerToken)
+ ->assertStatus(Response::HTTP_UNAUTHORIZED);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
diff --git a/tests/Unit/Auth/GuardTest.php b/tests/Unit/Auth/GuardTest.php
index 174d7fd7..8f28142a 100644
--- a/tests/Unit/Auth/GuardTest.php
+++ b/tests/Unit/Auth/GuardTest.php
@@ -1,3 +1,397 @@
group('auth', 'auth.guard', 'auth.guard.shared');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+ $this->config = $this->sdk->configuration();
+ $this->session = $this->config->getSessionStorage();
+
+ $this->user = new StatefulUser(['sub' => uniqid('auth0|')]);
+
+ Route::middleware('auth:auth0')->get('/test', function () {
+ return 'OK';
+ });
+});
+
+it('returns its configured name', function (): void {
+ expect($this->guard)
+ ->toBeInstanceOf(Guard::class)
+ ->getName()->toBe('legacyGuard');
+});
+
+it('assigns a user at login', function (): void {
+ expect($this->guard)
+ ->toBeInstanceOf(Guard::class)
+ ->user()->toBeNull();
+
+ $this->guard->login(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ expect($this->guard)
+ ->user()->toBe($this->user);
+
+ expect($this->guard)
+ ->id()->toBe($this->user->getAuthIdentifier());
+});
+
+it('logs out a user', function (): void {
+ expect($this->guard)
+ ->toBeInstanceOf(Guard::class)
+ ->user()->toBeNull();
+
+ $this->guard->login(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ expect($this->guard)
+ ->user()->toBe($this->user);
+
+ $this->guard->logout();
+
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ expect($this->guard)
+ ->id()->toBeNull();
+});
+
+it('forgets a user', function (): void {
+ expect($this->guard)
+ ->toBeInstanceOf(Guard::class)
+ ->user()->toBeNull();
+
+ $this->guard->login(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ expect($this->guard)
+ ->user()->toBe($this->user);
+
+ $this->guard->forgetUser();
+
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ expect($this->guard)
+ ->id()->toBeNull();
+});
+
+it('checks if a user is logged in', function (): void {
+ expect($this->guard)
+ ->check()->toBeFalse();
+
+ $this->guard->login(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ expect($this->guard)
+ ->check()->toBeTrue();
+});
+
+it('checks if a user is a guest', function (): void {
+ expect($this->guard)
+ ->guest()->toBeTrue();
+
+ $this->guard->login(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ expect($this->guard)
+ ->guest()->toBeFalse();
+});
+
+it('gets the user identifier', function (): void {
+ $this->guard->login(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ expect($this->guard)
+ ->id()->toBe($this->user->getAuthIdentifier());
+});
+
+it('validates a user', function (): void {
+ $this->guard->login(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ expect($this->guard)
+ ->validate(['id' => '123'])->toBeFalse()
+ ->validate(['id' => '456'])->toBeFalse();
+});
+
+it('gets/sets a user', function (): void {
+ $this->guard->setUser($this->user);
+
+ expect($this->guard)
+ ->user()->toBe($this->user);
+});
+
+it('has a user', function (): void {
+ $this->guard->setUser($this->user);
+
+ expect($this->guard)
+ ->hasUser()->toBeTrue();
+
+ $this->guard->logout();
+
+ expect($this->guard)
+ ->hasUser()->toBeFalse();
+});
+
+it('clears an imposter at logout', function (): void {
+ $this->guard->setImpersonating(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ expect($this->guard)
+ ->hasUser()->toBeTrue()
+ ->isImpersonating()->toBeTrue();
+
+ $this->guard->logout();
+
+ expect($this->guard)
+ ->isImpersonating()->toBeFalse()
+ ->hasUser()->toBeFalse();
+});
+
+it('has a scope', function (): void {
+ $this->user = new StatefulUser(['sub' => uniqid('auth0|'), 'scope' => 'read:users 456']);
+
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ accessTokenScope: ['read:users', '456']
+ );
+
+ expect($this->guard)
+ ->hasScope('read:users', $credential)->toBeTrue()
+ ->hasScope('123', $credential)->toBeFalse()
+ ->hasScope('456', $credential)->toBeTrue()
+ ->hasScope('789', $credential)->toBeFalse()
+ ->hasScope('*', $credential)->toBeTrue();
+
+ $credential = CredentialEntity::create(
+ user: $this->user
+ );
+
+ expect($this->guard)
+ ->hasScope('read:users', $credential)->toBeFalse()
+ ->hasScope('*', $credential)->toBeTrue();
+});
+
+it('checks if a user was authenticated via remember', function (): void {
+ $this->guard->login(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ expect($this->guard)
+ ->viaRemember()->toBeFalse();
+});
+
+it('returns null if authenticate() is called without being authenticated', function (): void {
+ $response = $this->guard->authenticate();
+ expect($response)->toBeNull();
+})->throws(AuthenticationException::class, AuthenticationException::UNAUTHENTICATED);
+
+it('returns a user from authenticate() if called while authenticated', function (): void {
+ $this->guard->login(CredentialEntity::create(
+ user: $this->user
+ ));
+
+ $response = $this->guard->authenticate();
+
+ expect($response)
+ ->toBe($this->user);
+});
+
+it('gets/sets a credentials', function (): void {
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->guard->setCredential($credential);
+
+ expect($this->guard)
+ ->user()->toBe($this->user);
+});
+
+it('queries the /userinfo endpoint', function (): void {
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->guard->setCredential($credential, Guard::SOURCE_TOKEN);
+
+ expect($this->guard)
+ ->user()->toBe($this->user);
+
+ $identifier = 'updated|' . uniqid();
+
+ $response = (new MockResponseFactory)->createResponse();
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new MockStreamFactory)->createStream(
+ json_encode(
+ value: [
+ 'sub' => $identifier,
+ 'name' => 'John Doe',
+ 'email' => '...',
+ ],
+ flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
+ )
+ )
+ )
+ );
+
+ $userAttributes = $this->guard->getRefreshedUser()->getAttributes();
+
+ expect($userAttributes)
+ ->toBeArray()
+ ->toMatchArray([
+ 'sub' => $identifier,
+ ]);
+});
+
+test('hasPermission(*) returns true for wildcard', function (): void {
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->guard->setCredential($credential, Guard::SOURCE_TOKEN);
+
+ expect($this->guard->hasPermission('*'))
+ ->toBeTrue();
+});
+
+test('hasPermission() returns true for matches', function (): void {
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenDecoded: [
+ 'permissions' => [
+ 'read:posts',
+ 'read:messages',
+ 'read:users',
+ ],
+ ],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->guard->setCredential($credential, Guard::SOURCE_TOKEN);
+
+ expect($this->guard->hasPermission('read:messages'))
+ ->toBeTrue();
+
+ expect($this->guard->hasPermission('write:posts'))
+ ->toBeFalse();
+});
+
+test('hasPermission() returns false when there are no permissions', function (): void {
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenDecoded: [
+ 'permissions' => [],
+ ],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->guard->setCredential($credential, Guard::SOURCE_TOKEN);
+
+ expect($this->guard->hasPermission('read:messages'))
+ ->toBeFalse();
+});
+
+test('management() returns a Management API class', function (): void {
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenDecoded: [
+ 'permissions' => [],
+ ],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->guard->setCredential($credential, Guard::SOURCE_TOKEN);
+
+ expect($this->guard->management())
+ ->toBeInstanceOf(ManagementInterface::class);
+});
+
+test('sdk() uses the guard name to optionally merge configuration data', function (): void {
+ config([
+ 'auth0.guards.default.domain' => 'https://default-domain.com',
+ 'auth0.guards.web.strategy' => 'none',
+ 'auth0.guards.web.domain' => 'https://legacy-domain.com',
+ ]);
+
+ expect($this->guard->sdk()->configuration()->getDomain())
+ ->toBe('legacy-domain.com');
+});
+
+test('sdk() configuration v1 is supported', function (): void {
+ config(['auth0' => [
+ 'strategy' => 'none',
+ 'domain' => 'https://v1-domain.com',
+ ]]);
+
+ expect($this->guard->sdk()->configuration()->getDomain())
+ ->toBe('v1-domain.com');
+});
+
+test('sdk() configuration v1 defaults to an empty array', function (): void {
+ config(['auth0' => 123]);
+ $this->guard->sdk()->configuration()->getDomain();
+})->throws(ConfigurationException::class);
diff --git a/tests/Unit/Auth0Test.php b/tests/Unit/Auth0Test.php
deleted file mode 100644
index 68dd5cd4..00000000
--- a/tests/Unit/Auth0Test.php
+++ /dev/null
@@ -1,54 +0,0 @@
-service)->
- toBeInstanceOf(\Auth0\Laravel\Auth0::class);
-}, );
-
-test('the service instantiates it\'s own configuration if none is assigned', function (): void {
- $service = new \Auth0\Laravel\Auth0();
-
- expect($service->getConfiguration())->
- toBeInstanceOf(\Auth0\SDK\Configuration\SdkConfiguration::class);
-}, );
-
-test('the service\'s getSdk() method returns an Auth0 SDK instance', function (): void {
- expect($this->service->getSdk())->
- toBeInstanceOf(\Auth0\SDK\Auth0::class);
-}, );
-
-test('the service\'s getConfiguration method returns an SdkConfiguration instance', function (): void {
- expect($this->service->getConfiguration())->
- toBeInstanceOf(\Auth0\SDK\Configuration\SdkConfiguration::class);
-}, );
-
-test('the service\'s getState method returns a StateInstance instance', function (): void {
- expect($this->service->getState())->
- toBeInstanceOf(\Auth0\Laravel\Contract\StateInstance::class);
-}, );
-
-test('the service\'s setSdk() method allows overwriting the Auth0 instance', function (): void {
- $oldSdk = $this->service->getSdk();
- $newSdk = createSdk();
-
- $this->service->setSdk($newSdk);
-
- expect($this->service->getSdk())->
- toBe($newSdk)->
- not()->
- toBe($oldSdk);
-}, );
-
-test('the service\'s setConfiguration() method allows overwriting the SdkConfiguration instance', function (): void {
- $oldConfiguration = $this->service->getConfiguration();
- $newConfiguration = createServiceConfiguration();
-
- $this->service->setConfiguration($newConfiguration);
-
- expect($this->service->getConfiguration())->
- toBe($newConfiguration)->
- not()->
- toBe($oldConfiguration);
-}, );
diff --git a/tests/Unit/Bridges/CacheBridgeTest.php b/tests/Unit/Bridges/CacheBridgeTest.php
new file mode 100644
index 00000000..58205617
--- /dev/null
+++ b/tests/Unit/Bridges/CacheBridgeTest.php
@@ -0,0 +1,255 @@
+group('cache', 'cache.laravel', 'cache.laravel.pool');
+
+test('getItem(), hasItem() and save() behave as expected', function (): void {
+ $pool = new CacheBridge();
+ $cache = $pool->getItem('testing');
+
+ expect($pool)
+ ->hasItem('testing')->toBeFalse();
+
+ expect($cache)
+ ->toBeInstanceOf(CacheItemBridge::class)
+ ->get()->toBeNull()
+ ->isHit()->toBeFalse();
+
+ $cache->set(42);
+
+ expect($cache)
+ ->get()->toBeNull();
+
+ expect($pool)
+ ->save($cache)->toBeTrue();
+
+ expect($pool)
+ ->hasItem('testing')->toBeTrue();
+
+ $cache = $pool->getItem('testing');
+
+ expect($cache)
+ ->toBeInstanceOf(CacheItemBridge::class)
+ ->isHit()->toBeTrue()
+ ->get()->toEqual(42);
+
+ $results = $pool->getItems();
+
+ expect($results)
+ ->toBeArray()
+ ->toHaveCount(0);
+
+ $results = $pool->getItems(['testing']);
+
+ expect($results['testing'])
+ ->toBeInstanceOf(CacheItemBridge::class)
+ ->isHit()->toBeTrue()
+ ->get()->toEqual(42);
+
+ $this->app[\Illuminate\Cache\CacheManager::class]
+ ->getStore()
+ ->put('testing', false, 60);
+
+ $cache = $pool->getItem('testing');
+
+ expect($pool)
+ ->hasItem('testing')->toBeFalse();
+
+ expect($cache)
+ ->toBeInstanceOf(CacheItemBridge::class)
+ ->get()->toBeNull()
+ ->isHit()->toBeFalse();
+
+ $cacheMock = Mockery::mock(CacheItemInterface::class);
+
+ expect($pool)
+ ->save($cacheMock)->toBeFalse();
+});
+
+test('save() with a negative expiration value is deleted', function (): void {
+ $pool = new CacheBridge();
+ $cache = new CacheItemBridge('testing', 42, true, new DateTime('now - 1 year'));
+
+ expect($pool)->hasItem('testing')->toBeFalse();
+
+ $pool->save($cache);
+
+ expect($pool)->hasItem('testing')->toBeFalse();
+});
+
+test('saveDeferred() behaves as expected', function (): void {
+ $pool = new CacheBridge();
+ $cache = new CacheItemBridge('testing', 42, true, new DateTime('now + 1 hour'));
+
+ expect($pool)
+ ->hasItem('testing')->toBeFalse()
+ ->saveDeferred($cache)->toBeTrue()
+ ->hasItem('testing')->toBeFalse()
+ ->commit()->toBeTrue()
+ ->hasItem('testing')->toBeTrue();
+});
+
+test('save() with a false value is discarded', function (): void {
+ $pool = new CacheBridge();
+ $cache = new CacheItemBridge('testing', false, true, new DateTime('now + 1 hour'));
+
+ expect($pool)
+ ->hasItem('testing')->toBeFalse()
+ ->save($cache)->toBeTrue()
+ ->hasItem('testing')->toBeFalse();
+});
+
+test('saveDeferred() returns false when the wrong type of interface is saved', function (): void {
+ $pool = new CacheBridge();
+ $cache = new CacheItemBridge('testing', 42, true, new DateTime('now + 1 hour'));
+
+ $cache = new class implements CacheItemInterface {
+ public function getKey(): string
+ {
+ return 'testing';
+ }
+
+ public function get(): mixed
+ {
+ return 42;
+ }
+
+ public function isHit(): bool
+ {
+ return true;
+ }
+
+ public function set(mixed $value): static
+ {
+ return $this;
+ }
+
+ public function expiresAt($expiration): static
+ {
+ return $this;
+ }
+
+ public function expiresAfter($time): static
+ {
+ return $this;
+ }
+ };
+
+ expect($pool)
+ ->hasItem('testing')->toBeFalse()
+ ->saveDeferred($cache)->toBeFalse()
+ ->hasItem('testing')->toBeFalse();
+});
+
+test('deleteItem() behaves as expected', function (): void {
+ $pool = new CacheBridge();
+ $cache = new CacheItemBridge('testing', 42, true, new DateTime('now + 1 minute'));
+
+ expect($pool)->hasItem('testing')->toBeFalse();
+
+ $pool->save($cache);
+
+ expect($pool)->hasItem('testing')->toBeTrue();
+
+ $cache = $pool->getItem('testing');
+
+ expect($cache)
+ ->isHit()->toBeTrue()
+ ->get()->toBe(42);
+
+ $pool->deleteItem('testing');
+
+ expect($pool)
+ ->hasItem('testing')->toBeFalse();
+
+ $cache = $pool->getItem('testing');
+
+ expect($cache)
+ ->isHit()->toBeFalse()
+ ->get()->toBeNull();
+});
+
+test('deleteItems() behaves as expected', function (): void {
+ $pool = new CacheBridge();
+
+ expect($pool)
+ ->hasItem('testing1')->toBeFalse()
+ ->hasItem('testing2')->toBeFalse()
+ ->hasItem('testing3')->toBeFalse();
+
+ $cache = new CacheItemBridge('testing1', uniqid(), true, new DateTime('now + 1 minute'));
+ $pool->save($cache);
+
+ $cache = new CacheItemBridge('testing2', uniqid(), true, new DateTime('now + 1 minute'));
+ $pool->save($cache);
+
+ $cache = new CacheItemBridge('testing3', uniqid(), true, new DateTime('now + 1 minute'));
+ $pool->save($cache);
+
+ expect($pool)
+ ->hasItem('testing1')->toBeTrue()
+ ->hasItem('testing2')->toBeTrue()
+ ->hasItem('testing3')->toBeTrue();
+
+ $results = $pool->getItems(['testing1' => 1, 'testing2' => 2, 'testing3' => 3]);
+
+ expect($results)
+ ->toHaveKey('testing1')
+ ->toHaveKey('testing2')
+ ->toHaveKey('testing3')
+ ->testing1->isHit()->toBeTrue()
+ ->testing2->isHit()->toBeTrue()
+ ->testing3->isHit()->toBeTrue();
+
+ expect($pool)
+ ->deleteItems(['testing1', 'testing2', 'testing3'])->toBeTrue()
+ ->hasItem('testing1')->toBeFalse()
+ ->hasItem('testing2')->toBeFalse()
+ ->hasItem('testing3')->toBeFalse()
+ ->deleteItems(['testing4', 'testing5', 'testing6'])->toBeFalse();
+});
+
+test('clear() behaves as expected', function (): void {
+ $pool = new CacheBridge();
+
+ expect($pool)
+ ->hasItem('testing1')->toBeFalse()
+ ->hasItem('testing2')->toBeFalse()
+ ->hasItem('testing3')->toBeFalse();
+
+ $cache = new CacheItemBridge('testing1', uniqid(), true, new DateTime('now + 1 minute'));
+ $pool->save($cache);
+
+ $cache = new CacheItemBridge('testing2', uniqid(), true, new DateTime('now + 1 minute'));
+ $pool->save($cache);
+
+ $cache = new CacheItemBridge('testing3', uniqid(), true, new DateTime('now + 1 minute'));
+ $pool->save($cache);
+
+ expect($pool)
+ ->hasItem('testing1')->toBeTrue()
+ ->hasItem('testing2')->toBeTrue()
+ ->hasItem('testing3')->toBeTrue();
+
+ $results = $pool->getItems(['testing1' => 1, 'testing2' => 2, 'testing3' => 3]);
+
+ expect($results)
+ ->toHaveKey('testing1')
+ ->toHaveKey('testing2')
+ ->toHaveKey('testing3')
+ ->testing1->isHit()->toBeTrue()
+ ->testing2->isHit()->toBeTrue()
+ ->testing3->isHit()->toBeTrue();
+
+ $pool->clear();
+
+ expect($pool)
+ ->hasItem('testing1')->toBeFalse()
+ ->hasItem('testing2')->toBeFalse()
+ ->hasItem('testing3')->toBeFalse();
+});
diff --git a/tests/Unit/Bridges/CacheItemBridgeTest.php b/tests/Unit/Bridges/CacheItemBridgeTest.php
new file mode 100644
index 00000000..ff00f699
--- /dev/null
+++ b/tests/Unit/Bridges/CacheItemBridgeTest.php
@@ -0,0 +1,100 @@
+group('cache', 'cache.laravel', 'cache.laravel.item');
+
+test('getKey() returns an expected value', function (): void {
+ $cacheItem = new CacheItemBridge('testing', 42, true);
+
+ expect($cacheItem->getKey())
+ ->toBe('testing');
+});
+
+test('get() returns an expected value when hit', function (): void {
+ $cacheItem = new CacheItemBridge('testing', 42, true);
+
+ expect($cacheItem->get())
+ ->toBe(42);
+});
+
+test('get() returns null when no hit', function (): void {
+ $cacheItem = new CacheItemBridge('testing', 42, false);
+
+ expect($cacheItem->get())
+ ->toBeNull();
+});
+
+test('getRawValue() returns an expected value', function (): void {
+ $cacheItem = new CacheItemBridge('testing', 42, false);
+
+ expect($cacheItem->getRawValue())
+ ->toBe(42);
+});
+
+test('isHit() returns an expected value when hit', function (): void {
+ $cacheItem = new CacheItemBridge('testing', 42, true);
+
+ expect($cacheItem->isHit())
+ ->toBeTrue();
+
+ $cacheItem = new CacheItemBridge('testing', 42, false);
+
+ expect($cacheItem->isHit())
+ ->toBeFalse();
+});
+
+test('set() alters the stored value as expected', function (): void {
+ $cacheItem = new CacheItemBridge('testing', 42, true);
+
+ expect($cacheItem->get())
+ ->toBe(42);
+
+ expect($cacheItem->set(43))
+ ->toBe($cacheItem)
+ ->get()->toBe(43);
+});
+
+test('expiresAt() defaults to +1 year and accepts changes to its value', function (): void {
+ $cacheItem = new CacheItemBridge('testing', 42, true);
+
+ expect($cacheItem->getExpiration()->getTimestamp())
+ ->toBeGreaterThan((new DateTime('now +1 year -1 minute'))->getTimestamp())
+ ->toBeLessThan((new DateTime('now +1 year +1 minute'))->getTimestamp());
+
+ $cacheItem->expiresAt(new DateTime('now +1 day'));
+
+ expect($cacheItem->getExpiration()->getTimestamp())
+ ->toBeGreaterThan((new DateTime('now +1 day -1 minute'))->getTimestamp())
+ ->toBeLessThan((new DateTime('now +1 day +1 minute'))->getTimestamp());
+});
+
+test('expiresAfter() defaults to +1 year and accepts changes to its value', function (): void {
+ $cacheItem = new CacheItemBridge('testing', 42, true);
+
+ expect($cacheItem->getExpiration()->getTimestamp())
+ ->toBeGreaterThan((new DateTime('now +1 year -1 minute'))->getTimestamp())
+ ->toBeLessThan((new DateTime('now +1 year +1 minute'))->getTimestamp());
+
+ $cacheItem->expiresAfter(300);
+
+ expect($cacheItem->getExpiration()->getTimestamp())
+ ->toBeGreaterThan((new DateTime('now +250 seconds'))->getTimestamp())
+ ->toBeLessThan((new DateTime('now +350 seconds'))->getTimestamp());
+});
+
+test('miss() returns a configured instance', function (): void {
+ $cacheItem = new CacheItemBridge('testing', 42, true);
+ $newCacheItem = $cacheItem->miss('testing123');
+
+ expect($cacheItem->getKey())
+ ->toBe('testing');
+
+ expect($newCacheItem->getKey())
+ ->toBe('testing123');
+
+ expect($newCacheItem->get())
+ ->not()->toBe($cacheItem->get());
+});
diff --git a/tests/Unit/Bridges/SessionBridgeTest.php b/tests/Unit/Bridges/SessionBridgeTest.php
new file mode 100644
index 00000000..2426d351
--- /dev/null
+++ b/tests/Unit/Bridges/SessionBridgeTest.php
@@ -0,0 +1,66 @@
+group('session-store');
+
+it('throws an exception when an empty prefix is provided', function (): void {
+ expect(function () {
+ new SessionBridge(
+ prefix: '',
+ );
+ })->toThrow(InvalidArgumentException::class);
+});
+
+it('accepts and uses a specified prefix', function (): void {
+ $prefix = uniqid();
+
+ $store = new SessionBridge(
+ prefix: $prefix,
+ );
+
+ expect($store)
+ ->toBeInstanceOf(StoreInterface::class)
+ ->getPrefix()->toBe($prefix);
+});
+
+it('allows updating the prefix', function (): void {
+ $store = new SessionBridge();
+
+ expect($store)
+ ->toBeInstanceOf(StoreInterface::class)
+ ->getPrefix()->toBe('auth0');
+
+ $prefix = uniqid();
+ $store->setPrefix($prefix);
+
+ expect($store)
+ ->toBeInstanceOf(StoreInterface::class)
+ ->getPrefix()->toBe($prefix);
+});
+
+it('supports CRUD operations', function (): void {
+ $prefix = uniqid();
+
+ $store = new SessionBridge(
+ prefix: $prefix,
+ );
+
+ expect($store)
+ ->toBeInstanceOf(StoreInterface::class)
+ ->get('test')->toBeNull()
+ ->set('test', 'value')->toBeNull()
+ ->get('test')->toBe('value')
+ ->getAll()->toBe(['test' => 'value'])
+ ->set('test2', 'value2')->toBeNull()
+ ->getAll()->toBe(['test' => 'value', 'test2' => 'value2'])
+ ->delete('test')->toBeNull()
+ ->getAll()->toBe(['test2' => 'value2'])
+ ->set('test3', 'value3')->toBeNull()
+ ->getAll()->toBe(['test2' => 'value2', 'test3' => 'value3'])
+ ->purge()->toBeNull()
+ ->getAll()->toBe([]);
+});
diff --git a/tests/Unit/ConfigurationTest.php b/tests/Unit/ConfigurationTest.php
new file mode 100644
index 00000000..795b709f
--- /dev/null
+++ b/tests/Unit/ConfigurationTest.php
@@ -0,0 +1,166 @@
+group('Configuration');
+
+test('stringToArrayOrNull() behaves as expected', function (): void {
+ expect(Configuration::stringToArrayOrNull('foo bar baz'))
+ ->toBe(['foo', 'bar', 'baz']);
+
+ expect(Configuration::stringToArrayOrNull(['foo', 'bar', 'baz']))
+ ->toBe(['foo', 'bar', 'baz']);
+
+ expect(Configuration::stringToArrayOrNull(' '))
+ ->toBeNull();
+});
+
+test('stringToArray() behaves as expected', function (): void {
+ expect(Configuration::stringToArray('foo bar baz'))
+ ->toBe(['foo', 'bar', 'baz']);
+
+ expect(Configuration::stringToArray(['foo', 'bar', 'baz']))
+ ->toBe(['foo', 'bar', 'baz']);
+
+ expect(Configuration::stringToArray(' '))
+ ->toBeArray()
+ ->toHaveCount(0);
+});
+
+test('stringToBoolOrNull() behaves as expected', function (): void {
+ expect(Configuration::stringToBoolOrNull('true'))
+ ->toBeTrue();
+
+ expect(Configuration::stringToBoolOrNull('false'))
+ ->toBeFalse();
+
+ expect(Configuration::stringToBoolOrNull('foo'))
+ ->toBeNull();
+
+ expect(Configuration::stringToBoolOrNull('foo', true))
+ ->toBeTrue();
+
+ expect(Configuration::stringToBoolOrNull('foo', false))
+ ->toBeFalse();
+});
+
+test('stringOrNull() behaves as expected', function (): void {
+ expect(Configuration::stringOrNull(123))
+ ->toBeNull();
+
+ expect(Configuration::stringOrNull(' 456 '))
+ ->toEqual('456');
+
+ expect(Configuration::stringOrNull(' '))
+ ->toBeNull();
+
+ expect(Configuration::stringOrNull('empty'))
+ ->toBeNull();
+
+ expect(Configuration::stringOrNull('(empty)'))
+ ->toBeNull();
+
+ expect(Configuration::stringOrNull('null'))
+ ->toBeNull();
+
+ expect(Configuration::stringOrNull('(null)'))
+ ->toBeNull();
+});
+
+test('stringOrIntToIntOrNull() behaves as expected', function (): void {
+ expect(Configuration::stringOrIntToIntOrNull(123))
+ ->toEqual(123);
+
+ expect(Configuration::stringOrIntToIntOrNull(' 456 '))
+ ->toEqual(456);
+
+ expect(Configuration::stringOrIntToIntOrNull(' '))
+ ->toBeNull();
+
+ expect(Configuration::stringOrIntToIntOrNull(' abc '))
+ ->toBeNull();
+});
+
+test('get() ignores quickstart placeholders', function (): void {
+ putenv('AUTH0_DOMAIN={DOMAIN}');
+ putenv('AUTH0_CLIENT_ID={CLIENT_ID}');
+ putenv('AUTH0_CLIENT_SECRET={CLIENT_SECRET}');
+ putenv('AUTH0_AUDIENCE={API_IDENTIFIER}');
+ putenv('AUTH0_CUSTOM_DOMAIN=https://example.com');
+
+ expect(Configuration::get(Configuration::CONFIG_CUSTOM_DOMAIN))
+ ->toBeString('https://example.com');
+
+ expect(Configuration::get(Configuration::CONFIG_DOMAIN))
+ ->toBeNull();
+
+ expect(Configuration::get(Configuration::CONFIG_CLIENT_ID))
+ ->toBeNull();
+
+ expect(Configuration::get(Configuration::CONFIG_CLIENT_SECRET))
+ ->toBeNull();
+
+ expect(Configuration::get(Configuration::CONFIG_AUDIENCE))
+ ->toBeNull();
+});
+
+test('get() behaves as expected', function (): void {
+ config(['test' => [
+ Configuration::CONFIG_AUDIENCE => implode(',', [uniqid(), uniqid()]),
+ Configuration::CONFIG_SCOPE => [],
+ Configuration::CONFIG_ORGANIZATION => '',
+
+ Configuration::CONFIG_USE_PKCE => true,
+ Configuration::CONFIG_HTTP_TELEMETRY => 'true',
+ Configuration::CONFIG_COOKIE_SECURE => 123,
+ Configuration::CONFIG_PUSHED_AUTHORIZATION_REQUEST => false,
+
+ 'tokenLeeway' => 123,
+ ]]);
+
+ define('AUTH0_OVERRIDE_CONFIGURATION', 'test');
+
+ expect(Configuration::get(Configuration::CONFIG_AUDIENCE))
+ ->toBeArray()
+ ->toHaveCount(2);
+
+ expect(Configuration::get(Configuration::CONFIG_SCOPE))
+ ->toBeNull();
+
+ expect(Configuration::get(Configuration::CONFIG_ORGANIZATION))
+ ->toBeNull();
+
+ expect(Configuration::get(Configuration::CONFIG_USE_PKCE))
+ ->toBeTrue();
+
+ expect(Configuration::get(Configuration::CONFIG_HTTP_TELEMETRY))
+ ->toBeTrue();
+
+ expect(Configuration::get(Configuration::CONFIG_COOKIE_SECURE))
+ ->toBeNull();
+
+ expect(Configuration::get(Configuration::CONFIG_PUSHED_AUTHORIZATION_REQUEST))
+ ->toBeFalse();
+
+ expect(Configuration::get('tokenLeeway'))
+ ->toBeInt()
+ ->toEqual(123);
+});
+
+test('string() behaves as expected', function (): void {
+ config(['test2' => [
+ 'testInteger' => 123,
+ 'testString' => '123',
+ ]]);
+
+ define('AUTH0_OVERRIDE_CONFIGURATION_STRING_METHOD', 'test2');
+
+ expect(Configuration::string('test2.testInteger'))
+ ->toBeNull();
+
+ expect(Configuration::string('test2.testString'))
+ ->toBeString()
+ ->toEqual('123');
+});
diff --git a/tests/Unit/Controllers/CallbackControllerTest.php b/tests/Unit/Controllers/CallbackControllerTest.php
new file mode 100644
index 00000000..99a2ca48
--- /dev/null
+++ b/tests/Unit/Controllers/CallbackControllerTest.php
@@ -0,0 +1,162 @@
+group('stateful', 'controller', 'controller.stateful', 'controller.stateful.callback');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->guard->sdk();
+ $this->config = $this->guard->sdk()->configuration();
+ $this->user = new ImposterUser(['sub' => uniqid('auth0|')]);
+
+ Route::get('/auth0/callback', CallbackController::class)->name('callback');
+});
+
+it('redirects home if an incompatible guard is active', function (): void {
+ config([
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null,
+ ]);
+
+ expect(function () {
+ $this->withoutExceptionHandling()
+ ->getJson('/auth0/callback')
+ ->assertStatus(Response::HTTP_INTERNAL_SERVER_ERROR);
+ })->toThrow(ControllerException::class);
+});
+
+it('accepts code and state parameters', function (): void {
+ expect(function () {
+ $this->withoutExceptionHandling()
+ ->getJson('/auth0/callback?code=code&state=state');
+ })->toThrow(StateException::class);
+
+ $this->assertDispatched(Attempting::class, 1);
+ $this->assertDispatched(Failed::class, 1);
+ $this->assertDispatched(AuthenticationFailed::class, 1);
+
+ $this->assertDispatchedOrdered([
+ Attempting::class,
+ Failed::class,
+ AuthenticationFailed::class,
+ ]);
+});
+
+it('accepts error and error_description parameters', function (): void {
+ expect(function () {
+ $this->withoutExceptionHandling()
+ ->getJson('/auth0/callback?error=123&error_description=456');
+ })->toThrow(CallbackControllerException::class);
+
+ $this->assertDispatched(Attempting::class, 1);
+ $this->assertDispatched(Failed::class, 1);
+ $this->assertDispatched(AuthenticationFailed::class, 1);
+
+ $this->assertDispatchedOrdered([
+ Attempting::class,
+ Failed::class,
+ AuthenticationFailed::class,
+ ]);
+});
+
+it('returns a user and sets up a session', function (): void {
+ $this->config->setTokenAlgorithm(Token::ALGO_HS256);
+
+ $state = uniqid();
+ $pkce = uniqid();
+ $nonce = uniqid();
+ $verifier = uniqid();
+
+ $idToken = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ 'sub' => 'hello|world',
+ 'aud' => config('auth0.guards.default.clientId'),
+ 'exp' => time() + 60,
+ 'iat' => time(),
+ 'email' => 'john.doe@somewhere.test',
+ 'nonce' => $nonce
+ ], []);
+
+ $accessToken = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ 'sub' => 'hello|world',
+ 'aud' => config('auth0.guards.default.clientId'),
+ 'iat' => time(),
+ 'exp' => time() + 60,
+ 'azp' => config('auth0.guards.default.clientId'),
+ 'scope' => 'openid profile email'
+ ], []);
+
+ $factory = $this->config->getHttpResponseFactory();
+ $response = $factory->createResponse();
+ $response->getBody()->write(json_encode([
+ 'access_token' => $accessToken->toString(),
+ 'id_token' => $idToken->toString(),
+ 'scope' => 'openid profile email',
+ 'expires_in' => 60,
+ ]));
+
+ $client = $this->config->getHttpClient();
+ $client->addResponse('POST', 'https://' . config('auth0.guards.default.domain') . '/oauth/token', $response);
+
+ $this->withSession([
+ 'auth0_transient' => json_encode([
+ 'state' => $state,
+ 'pkce' => $pkce,
+ 'nonce' => $nonce,
+ 'code_verifier' => $verifier
+ ])
+ ])->getJson('/auth0/callback?code=code&state=' . $state)
+ ->assertFound()
+ ->assertLocation('/');
+
+ $this->assertDispatched(Attempting::class, 1);
+ $this->assertDispatched(Validated::class, 1);
+ $this->assertDispatched(Login::class, 1);
+ $this->assertDispatched(AuthenticationSucceeded::class, 1);
+ $this->assertDispatched(Authenticated::class, 1);
+
+ $this->assertDispatchedOrdered([
+ Attempting::class,
+ Validated::class,
+ Login::class,
+ AuthenticationSucceeded::class,
+ Authenticated::class,
+ ]);
+});
+
+it('redirects visitors if an expected parameter is not provided', function (): void {
+ $this->getJson('/auth0/callback?code=code')
+ ->assertFound()
+ ->assertLocation('/login');
+});
diff --git a/tests/Unit/Controllers/LoginControllerTest.php b/tests/Unit/Controllers/LoginControllerTest.php
new file mode 100644
index 00000000..2a188c05
--- /dev/null
+++ b/tests/Unit/Controllers/LoginControllerTest.php
@@ -0,0 +1,66 @@
+group('stateful', 'controller', 'controller.stateful', 'controller.stateful.login');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $this->validSession = [
+ 'auth0_session' => json_encode([
+ 'user' => ['sub' => 'hello|world'],
+ 'idToken' => (string) Generator::create((createRsaKeys())->private),
+ 'accessToken' => (string) Generator::create((createRsaKeys())->private),
+ 'accessTokenScope' => [uniqid()],
+ 'accessTokenExpiration' => time() + 60,
+ ])
+ ];
+
+ Route::get('/login', LoginController::class);
+});
+
+it('redirects to the home route if an incompatible guard is active', function (): void {
+ config($config = [
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null,
+ ]);
+
+ expect(function () {
+ $this->withoutExceptionHandling()
+ ->getJson('/login')
+ ->assertStatus(Response::HTTP_INTERNAL_SERVER_ERROR);
+ })->toThrow(ControllerException::class);
+});
+
+it('redirects to the home route when a user is already logged in', function (): void {
+ $this->withSession($this->validSession)
+ ->get('/login')
+ ->assertRedirect('/');
+});
+
+it('redirects to the Universal Login Page', function (): void {
+ $this->get('/login')
+ ->assertRedirectContains('/authorize');
+});
diff --git a/tests/Unit/Controllers/LogoutControllerTest.php b/tests/Unit/Controllers/LogoutControllerTest.php
new file mode 100644
index 00000000..5225f37a
--- /dev/null
+++ b/tests/Unit/Controllers/LogoutControllerTest.php
@@ -0,0 +1,65 @@
+group('stateful', 'controller', 'controller.stateful', 'controller.stateful.logout');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $this->validSession = [
+ 'auth0_session' => json_encode([
+ 'user' => ['sub' => 'hello|world'],
+ 'idToken' => (string) Generator::create((createRsaKeys())->private),
+ 'accessToken' => (string) Generator::create((createRsaKeys())->private),
+ 'accessTokenScope' => [uniqid()],
+ 'accessTokenExpiration' => time() + 60,
+ ])
+ ];
+
+ Route::get('/logout', LogoutController::class);
+});
+
+it('redirects to the home route if an incompatible guard is active', function (): void {
+ config($config = [
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null
+ ]);
+
+ expect(function () {
+ $this->withoutExceptionHandling()
+ ->getJson('/logout')
+ ->assertStatus(Response::HTTP_INTERNAL_SERVER_ERROR);
+ })->toThrow(ControllerException::class);
+});
+
+it('redirects to the home route when a user is not already logged in', function (): void {
+ $this->get('/logout')
+ ->assertRedirect('/');
+});
+
+it('redirects to the Auth0 logout endpoint', function (): void {
+ $this->withSession($this->validSession)
+ ->get('/logout')
+ ->assertRedirectContains('/v2/logout');
+});
diff --git a/tests/Unit/Entities/CredentialEntityTest.php b/tests/Unit/Entities/CredentialEntityTest.php
new file mode 100644
index 00000000..3b0c491f
--- /dev/null
+++ b/tests/Unit/Entities/CredentialEntityTest.php
@@ -0,0 +1,205 @@
+group('stateful', 'model', 'model.credential');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_API,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.audience' => [uniqid()],
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $this->user = new StatelessUser(['sub' => uniqid('auth0|')]);
+ $this->idToken = mockIdToken(algorithm: Token::ALGO_HS256);
+ $this->accessToken = mockAccessToken(algorithm: Token::ALGO_HS256);
+ $this->accessTokenScope = ['openid', 'profile', 'email', uniqid()];
+ $this->accessTokenDecoded = [uniqid(), ['hello' => 'world']];
+ $this->accessTokenExpiration = time() + 3600;
+ $this->refreshToken = uniqid();
+});
+
+test('create() returns a properly configured instance', function (): void {
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ idToken: $this->idToken,
+ accessToken: $this->accessToken,
+ accessTokenScope: $this->accessTokenScope,
+ accessTokenExpiration: $this->accessTokenExpiration,
+ refreshToken: $this->refreshToken
+ );
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getUser()->toBe($this->user)
+ ->getIdToken()->toBe($this->idToken)
+ ->getAccessToken()->toBe($this->accessToken)
+ ->getAccessTokenScope()->toBe($this->accessTokenScope)
+ ->getAccessTokenExpiration()->toBe($this->accessTokenExpiration)
+ ->getRefreshToken()->toBe($this->refreshToken);
+});
+
+it('clear() nullifies all properties', function (): void {
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ idToken: $this->idToken,
+ accessToken: $this->accessToken,
+ accessTokenScope: $this->accessTokenScope,
+ accessTokenExpiration: $this->accessTokenExpiration,
+ refreshToken: $this->refreshToken,
+ );
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getUser()->toBe($this->user)
+ ->getIdToken()->toBe($this->idToken)
+ ->getAccessToken()->toBe($this->accessToken)
+ ->getAccessTokenScope()->toBe($this->accessTokenScope)
+ ->getAccessTokenExpiration()->toBe($this->accessTokenExpiration)
+ ->getRefreshToken()->toBe($this->refreshToken);
+
+ expect($credential->clear())
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getUser()->toBeNull()
+ ->getIdToken()->toBeNull()
+ ->getAccessToken()->toBeNull()
+ ->getAccessTokenScope()->toBeNull()
+ ->getAccessTokenExpiration()->toBeNull()
+ ->getRefreshToken()->toBeNull();
+});
+
+it('setUser() assigns a correct value', function (): void {
+ $credential = CredentialEntity::create();
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getUser()->toBeNull();
+
+ expect($credential->setUser($this->user))
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getUser()->toBe($this->user);
+});
+
+it('setIdToken() assigns a correct value', function (): void {
+ $credential = CredentialEntity::create();
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getIdToken()->toBeNull();
+
+ expect($credential->setIdToken($this->idToken))
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getIdToken()->toBe($this->idToken);
+});
+
+it('setAccessToken() assigns a correct value', function (): void {
+ $credential = CredentialEntity::create();
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessToken()->toBeNull();
+
+ expect($credential->setAccessToken($this->accessToken))
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessToken()->toBe($this->accessToken);
+});
+
+it('setAccessTokenScope() assigns a correct value', function (): void {
+ $credential = CredentialEntity::create();
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessTokenScope()->toBeNull();
+
+ expect($credential->setAccessTokenScope($this->accessTokenScope))
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessTokenScope()->toBe($this->accessTokenScope);
+});
+
+it('setAccessTokenDecoded() assigns a correct value', function (): void {
+ $credential = CredentialEntity::create();
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessTokenDecoded()->toBeNull();
+
+ expect($credential->setAccessTokenDecoded($this->accessTokenDecoded))
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessTokenDecoded()->toBe($this->accessTokenDecoded);
+});
+
+it('setAccessTokenExpiration() assigns a correct value', function (): void {
+ $credential = CredentialEntity::create();
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessTokenExpiration()->toBeNull();
+
+ expect($credential->setAccessTokenExpiration($this->accessTokenExpiration))
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessTokenExpiration()->toBe($this->accessTokenExpiration);
+});
+
+it('setRefreshToken() assigns a correct value', function (): void {
+ $credential = CredentialEntity::create();
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getRefreshToken()->toBeNull();
+
+ expect($credential->setRefreshToken($this->refreshToken))
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getRefreshToken()->toBe($this->refreshToken);
+});
+
+it('getAccessTokenExpired() returns a correct value', function (): void {
+ $credential = CredentialEntity::create();
+
+ expect($credential)
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessTokenExpired()->toBeNull();
+
+ expect($credential->setAccessTokenExpiration($this->accessTokenExpiration))
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessTokenExpired()->toBeFalse();
+
+ expect($credential->setAccessTokenExpiration($this->accessTokenExpiration - 3600 * 2))
+ ->toBeInstanceOf(CredentialEntity::class)
+ ->getAccessTokenExpired()->toBeTrue();
+});
+
+it('jsonSerialize() returns a correct structure', function (): void {
+ $credential = CredentialEntity::create(
+ user: $this->user,
+ idToken: $this->idToken,
+ accessToken: $this->accessToken,
+ accessTokenScope: $this->accessTokenScope,
+ accessTokenExpiration: $this->accessTokenExpiration,
+ refreshToken: $this->refreshToken,
+ );
+
+ expect(json_encode($credential))
+ ->json()
+ ->user->toBe(json_encode($this->user))
+ ->idToken->toBe($this->idToken)
+ ->accessToken->toBe($this->accessToken)
+ ->accessTokenScope->toBe($this->accessTokenScope)
+ ->accessTokenExpiration->toBe($this->accessTokenExpiration)
+ ->refreshToken->toBe($this->refreshToken);
+});
diff --git a/tests/Unit/Entities/InstanceEntityTest.php b/tests/Unit/Entities/InstanceEntityTest.php
new file mode 100644
index 00000000..58c7cc3b
--- /dev/null
+++ b/tests/Unit/Entities/InstanceEntityTest.php
@@ -0,0 +1,62 @@
+group('Entities/InstanceEntity');
+
+beforeEach(function (): void {
+});
+
+it('instantiates an empty configuration if a non-array is supplied', function (): void {
+ config(['auth0' => true]);
+
+ (new InstanceEntity())->getConfiguration();
+})->throws(ConfigurationException::class);
+
+test('setGuardConfigurationKey() sets the guard configuration key', function (): void {
+ $key = uniqid();
+ $instance = new InstanceEntity();
+ $instance->setGuardConfigurationKey($key);
+
+ expect($instance->getGuardConfigurationKey())
+ ->toBe($key);
+});
+
+test('setConfiguration sets the configuration using an SdkConfiguration', function (): void {
+ $instance = new InstanceEntity();
+ $configuration = new SdkConfiguration(['strategy' => 'none', 'domain' => uniqid() . '.auth0.test']);
+
+ $instance->setConfiguration($configuration);
+
+ expect($instance->getConfiguration())
+ ->toBe($configuration);
+});
+
+test('setConfiguration sets the configuration using an array', function (): void {
+ $instance = new InstanceEntity();
+ $configuration = ['strategy' => 'none', 'domain' => uniqid() . '.auth0.test'];
+
+ $instance->setConfiguration($configuration);
+
+ expect($instance->getConfiguration())
+ ->toBeInstanceOf(SdkConfiguration::class);
+});
+
+test('::create() sets the guard configuration key and configuration', function (): void {
+ $key = uniqid();
+ $configuration = new SdkConfiguration(['strategy' => 'none', 'domain' => uniqid() . '.auth0.test']);
+ $instance = InstanceEntity::create(
+ configuration: $configuration,
+ guardConfigurationName: $key,
+ );
+
+ expect($instance->getConfiguration())
+ ->toBe($configuration);
+
+ expect($instance->getGuardConfigurationKey())
+ ->toBe($key);
+});
diff --git a/tests/Unit/Guards/AuthenticationGuardTest.php b/tests/Unit/Guards/AuthenticationGuardTest.php
new file mode 100644
index 00000000..9796c5fa
--- /dev/null
+++ b/tests/Unit/Guards/AuthenticationGuardTest.php
@@ -0,0 +1,432 @@
+group('auth', 'auth.guard', 'auth.guard.session');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = $guard = auth('auth0-session');
+ $this->sdk = $this->laravel->getSdk();
+ $this->config = $this->sdk->configuration();
+ $this->session = $this->config->getSessionStorage();
+
+ $this->user = new StatefulUser(['sub' => uniqid('auth0|')]);
+
+ $this->session->set('user', ['sub' => 'hello|world']);
+ $this->session->set('idToken', (string) Generator::create((createRsaKeys())->private));
+ $this->session->set('accessToken', (string) Generator::create((createRsaKeys())->private));
+ $this->session->set('accessTokenScope', [uniqid()]);
+ $this->session->set('accessTokenExpiration', time() + 60);
+
+ $this->route = '/' . uniqid();
+ $this->route2 = '/' . uniqid();
+ $guard = $this->guard;
+
+ Route::get($this->route, function () use ($guard) {
+ $credential = $guard->find(Guard::SOURCE_SESSION);
+
+ if (null !== $credential) {
+ $guard->login($credential, Guard::SOURCE_SESSION);
+ return response()->json(['status' => 'OK']);
+ }
+
+ abort(Response::HTTP_UNAUTHORIZED, 'Unauthorized');
+ });
+
+ Route::get($this->route2, function () use ($guard) {
+ return response()->json(['user' => $guard->user(Guard::SOURCE_SESSION)->getAuthIdentifier()]);
+ });
+});
+
+it('retrieves the authenticated user from a valid session using find()', function (): void {
+ $result = $this->guard->find();
+
+ expect($result)->toBeInstanceOf(CredentialEntity::class);
+ expect($result->getUser())->toBeInstanceOf(StatefulUser::class);
+
+ expect($this->guard->user()->getAuthIdentifier())->toBe('hello|world');
+});
+
+it('retrieves the authenticated user from a valid session using user()', function (): void {
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe('hello|world');
+});
+
+it('updates internal and session states as appropriate', function (): void {
+ // Session should be available and populated
+ expect($this->session)
+ ->getAll()->not()->toBe([]);
+
+ // Guard should pick up on the session during the HTTP request
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ // Guard should have it's state populated
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe('hello|world');
+
+ // Empty guard state
+ $this->guard->logout();
+
+ // Guard should have had it's state emptied
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ // Session should have been emptied
+ expect($this->session)
+ ->getAll()->toBe([]);
+
+ // HTTP request should fail without a session.
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_UNAUTHORIZED);
+
+ // Inject a new session into the store
+ $this->session->set('user', ['sub' => 'hello|world|two']);
+
+ // Session should be available and populated again
+ expect($this->session)
+ ->getAll()->not()->toBe([]);
+
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ // Guard should pick up on the session
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe('hello|world|two');
+
+ // Directly wipe the Laravel session, circumventing the Guard APIs
+ $this->session->purge();
+
+ // Session should be empty
+ expect($this->session)
+ ->getAll()->toBe([]);
+
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_UNAUTHORIZED);
+
+ // Guard should have it's state emptied
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ $this->session->set('user', ['sub' => 'hello|world|4']);
+
+ // Session should be available
+ expect($this->session)
+ ->getAll()->not()->toBe([]);
+
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ // Guard should pick up on the session
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe('hello|world|4');
+
+ $identifier = uniqid('auth0|');
+ $user = new StatefulUser(['sub' => $identifier]);
+
+ // Overwrite state using the Guard's login()
+ $this->guard->login(CredentialEntity::create(
+ user: $user
+ ), Guard::SOURCE_SESSION);
+
+ getJson($this->route)
+ ->assertStatus(Response::HTTP_OK);
+
+ // Guard should have it's state updated
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe($identifier);
+
+ // Session should be updated
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier]);
+});
+
+it('creates a session from login()', function (): void {
+ $identifier = uniqid('auth0|');
+ $accessTokenScope = [uniqid('access-token-scope-')];
+ $accessTokenExpiration = time() + 60;
+
+ $this->session->set('user', ['sub' => $identifier]);
+ $this->session->set('accessTokenScope', $accessTokenScope);
+ $this->session->set('accessTokenExpiration', $accessTokenExpiration);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+
+ expect($found)
+ ->toBeInstanceOf(CredentialEntity::class);
+
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier])
+ ->get('accessTokenScope')->toBe($accessTokenScope)
+ ->get('accessTokenExpiration')->toBe($accessTokenExpiration)
+ ->get('refreshToken')->toBeNull();
+
+ $user = new StatefulUser(['sub' => $identifier]);
+
+ $changedRefreshToken = (string) Generator::create((createRsaKeys())->private);
+
+ // Overwrite state using the Guard's login()
+ $this->guard->login(CredentialEntity::create(
+ user: $user,
+ refreshToken: $changedRefreshToken
+ ), Guard::SOURCE_SESSION);
+
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe($identifier);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier])
+ ->get('accessTokenScope')->toBe($accessTokenScope)
+ ->get('accessTokenExpiration')->toBe($accessTokenExpiration)
+ ->get('refreshToken')->toBe($changedRefreshToken);
+});
+
+it('queries the /userinfo endpoint for refreshUser()', function (): void {
+ $identifier = uniqid('auth0|');
+ $accessTokenScope = [uniqid('access-token-scope-')];
+ $accessTokenExpiration = time() + 60;
+
+ $this->session->set('user', ['sub' => $identifier]);
+ $this->session->set('accessTokenScope', $accessTokenScope);
+ $this->session->set('accessTokenExpiration', $accessTokenExpiration);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier]);
+
+ $response = (new ResponseFactory)->createResponse();
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new StreamFactory)->createStream(
+ json_encode(
+ value: [
+ 'sub' => $identifier,
+ 'name' => 'John Doe',
+ 'email' => '...',
+ ],
+ flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
+ )
+ )
+ )
+ );
+
+ $this->guard->refreshUser();
+
+ $userAttributes = $this->guard->user()->getAttributes();
+
+ expect($userAttributes)
+ ->toBeArray()
+ ->toMatchArray([
+ 'sub' => $identifier,
+ 'name' => 'John Doe',
+ 'email' => '...',
+ ]);
+});
+
+it('does not query the /userinfo endpoint for refreshUser() if an access token is not available', function (): void {
+ $identifier = uniqid('auth0|');
+ $accessTokenScope = [uniqid('access-token-scope-')];
+ $accessTokenExpiration = time() + 60;
+
+ $this->session->set('user', ['sub' => $identifier]);
+ $this->session->set('accessToken', null);
+ $this->session->set('accessTokenScope', $accessTokenScope);
+ $this->session->set('accessTokenExpiration', $accessTokenExpiration);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier]);
+
+ $response = (new ResponseFactory)->createResponse();
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new StreamFactory)->createStream(
+ json_encode(
+ value: [
+ 'sub' => $identifier,
+ 'name' => 'John Doe',
+ 'email' => '...',
+ ],
+ flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
+ )
+ )
+ )
+ );
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->setRequestLimit(-1);
+
+ $this->guard->refreshUser();
+
+ $userAttributes = $this->guard->user()->getAttributes();
+
+ expect($userAttributes)
+ ->toBeArray()
+ ->toMatchArray([
+ 'sub' => $identifier,
+ ]);
+});
+
+it('rejects bad responses from the /userinfo endpoint for refreshUser()', function (): void {
+ $identifier = uniqid('auth0|');
+ $accessTokenScope = [uniqid('access-token-scope-')];
+ $accessTokenExpiration = time() + 60;
+
+ $this->session->set('user', ['sub' => $identifier]);
+ $this->session->set('accessTokenScope', $accessTokenScope);
+ $this->session->set('accessTokenExpiration', $accessTokenExpiration);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->session)
+ ->get('user')->toBe(['sub' => $identifier]);
+
+ $response = (new ResponseFactory)->createResponse();
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new StreamFactory)->createStream(
+ json_encode(
+ value: 'bad response',
+ flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
+ )
+ )
+ )
+ );
+
+ $this->guard->refreshUser();
+
+ $userAttributes = $this->guard->user()->getAttributes();
+
+ expect($userAttributes)
+ ->toBeArray()
+ ->toMatchArray([
+ 'sub' => $identifier,
+ ]);
+});
+
+it('immediately invalidates an expired session when a refresh token is not available', function (): void {
+ $this->session->set('accessTokenExpiration', time() - 1000);
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ expect($this->session)
+ ->get('user')->toBeNull();
+});
+
+it('invalidates an expired session when an access token fails to refresh', function (): void {
+ $this->session->set('accessTokenExpiration', time() - 1000);
+ $this->session->set('refreshToken', uniqid());
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ expect($this->session)
+ ->get('user')->toBeNull();
+});
+
+it('successfully continues a session when an access token succeeds is renewed', function (): void {
+ $this->session->set('accessTokenExpiration', time() - 1000);
+ $this->session->set('refreshToken', uniqid());
+
+ $response = (new ResponseFactory)->createResponse();
+
+ $token = Generator::create((createRsaKeys())->private, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ "sub" => "auth0|123456",
+ "aud" => [
+ "https://example.com/health-api",
+ "https://my-domain.auth0.com/userinfo",
+ config('auth0.guards.default.clientId')
+ ],
+ "azp" => config('auth0.guards.default.clientId'),
+ "exp" => time() + 60,
+ "iat" => time(),
+ "scope" => "openid profile read:patients read:admin"
+ ]);
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new StreamFactory)->createStream(
+ json_encode(
+ value: [
+ 'access_token' => $token->toString(),
+ 'expires_in' => 60,
+ 'scope' => 'openid profile',
+ 'token_type' => 'Bearer',
+ ],
+ flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
+ )
+ )
+ )
+ );
+
+ $found = $this->guard->find(Guard::SOURCE_SESSION);
+ $this->guard->login($found, Guard::SOURCE_SESSION);
+
+ expect($this->guard)
+ ->user()->not()->toBeNull();
+
+ expect($this->session)
+ ->get('user')->not()->toBeNull();
+});
diff --git a/tests/Unit/Guards/AuthorizationGuardTest.php b/tests/Unit/Guards/AuthorizationGuardTest.php
new file mode 100644
index 00000000..4d8a7c6e
--- /dev/null
+++ b/tests/Unit/Guards/AuthorizationGuardTest.php
@@ -0,0 +1,218 @@
+group('auth', 'auth.guard', 'auth.guard.session');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_API,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.audience' => ['https://example.com/health-api'],
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = $guard = auth('auth0-api');
+ $this->sdk = $this->laravel->getSdk();
+ $this->config = $this->sdk->configuration();
+
+ $this->identifier = 'auth0|' . uniqid();
+
+ $this->token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ "sub" => $this->identifier,
+ "aud" => [
+ config('auth0.guards.default.audience')[0],
+ "https://my-domain.auth0.com/userinfo"
+ ],
+ "azp" => config('auth0.guards.default.clientId'),
+ "exp" => time() + 60,
+ "iat" => time(),
+ "scope" => "openid profile read:patients read:admin"
+ ]);
+
+ $this->bearerToken = ['Authorization' => 'Bearer ' . $this->token->toString()];
+
+ $this->route = '/' . uniqid();
+ $this->route2 = '/' . uniqid();
+ $guard = $this->guard;
+
+ Route::get($this->route, function () use ($guard) {
+ $credential = $guard->find(Guard::SOURCE_TOKEN);
+
+ if (null !== $credential) {
+ $guard->login($credential, Guard::SOURCE_TOKEN);
+
+ return response()->json(['status' => 'OK', 'user' => $guard->user(Guard::SOURCE_TOKEN)->getAuthIdentifier()]);
+ }
+
+ abort(Response::HTTP_UNAUTHORIZED, 'Unauthorized');
+ });
+
+ Route::get($this->route2, function () use ($guard) {
+ return response()->json(['user' => $guard->user(Guard::SOURCE_TOKEN)?->getAuthIdentifier()]);
+ });
+});
+
+it('assigns a user with login() from a good token', function (): void {
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ getJson($this->route, $this->bearerToken)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($this->guard)
+ ->user()->not()->toBeNull();
+});
+
+it('assigns a user with user() from a good token', function (): void {
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ getJson($this->route2, $this->bearerToken)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($this->guard)
+ ->user()->not()->toBeNull();
+});
+
+// it('does not assign a user from a empty token', function (): void {
+// getJson($this->route, ['Authorization' => 'Bearer '])
+// ->assertStatus(Response::HTTP_UNAUTHORIZED);
+
+// expect($this->guard)
+// ->user()->toBeNull();
+// });
+
+// it('does not get a user from a bad token', function (): void {
+// $this->config->setAudience(['BAD_AUDIENCE']);
+
+// expect($this->guard)
+// ->user()->toBeNull();
+
+// getJson($this->route, $this->bearerToken)
+// ->assertStatus(Response::HTTP_UNAUTHORIZED);
+
+// expect($this->guard)
+// ->user()->toBeNull();
+// });
+
+it('does not query the /userinfo endpoint for refreshUser() without a bearer token', function (): void {
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ $this->guard->setCredential(new CredentialEntity(
+ user: new StatelessUser(['sub' => $this->identifier]),
+ ));
+
+ expect($this->guard)
+ ->user()->not()->toBeNull();
+
+ $client = new MockHttpClient(requestLimit: 0);
+
+ $this->config->setHttpClient($client);
+
+ $this->guard->refreshUser();
+
+ expect($this->guard)
+ ->user()->not()->toBeNull();
+});
+
+it('aborts querying the /userinfo endpoint for refreshUser() when a bad response is received', function (): void {
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ getJson($this->route2, $this->bearerToken)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe($this->identifier);
+
+ $response = (new MockResponseFactory)->createResponse();
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new MockStreamFactory)->createStream(
+ json_encode(
+ value: true,
+ flags: JSON_PRETTY_PRINT
+ )
+ )
+ )
+ );
+
+ $this->guard->refreshUser();
+
+ $userAttributes = $this->guard->user()->getAttributes();
+
+ expect($userAttributes)
+ ->toBeArray()
+ ->toHaveKey('sub', $this->identifier);
+});
+
+it('queries the /userinfo endpoint for refreshUser()', function (): void {
+ expect($this->guard)
+ ->user()->toBeNull();
+
+ getJson($this->route2, $this->bearerToken)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($this->guard)
+ ->user()->getAuthIdentifier()->toBe($this->identifier);
+
+ $response = (new MockResponseFactory)->createResponse();
+
+ $this->guard
+ ->sdk()
+ ->configuration()
+ ->getHttpClient()
+ ->addResponseWildcard($response->withBody(
+ (new MockStreamFactory)->createStream(
+ json_encode(
+ value: [
+ 'sub' => $this->identifier,
+ 'name' => 'John Doe',
+ 'email' => '...',
+ ],
+ flags: JSON_PRETTY_PRINT
+ )
+ )
+ )
+ );
+
+ $this->guard->refreshUser();
+
+ $userAttributes = $this->guard->user()->getAttributes();
+
+ expect($userAttributes)
+ ->toBeArray()
+ ->toMatchArray([
+ 'sub' => $this->identifier,
+ 'name' => 'John Doe',
+ 'email' => '...',
+ ]);
+});
diff --git a/tests/Unit/Middleware/AuthenticateMiddlewareTest.php b/tests/Unit/Middleware/AuthenticateMiddlewareTest.php
new file mode 100644
index 00000000..7cfab85f
--- /dev/null
+++ b/tests/Unit/Middleware/AuthenticateMiddlewareTest.php
@@ -0,0 +1,122 @@
+group('stateful', 'middleware', 'middleware.stateful', 'middleware.stateful.authenticate');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $this->validSession = [
+ 'auth0_session' => json_encode([
+ 'user' => ['sub' => 'hello|world'],
+ 'idToken' => (string) Generator::create((createRsaKeys())->private),
+ 'accessToken' => (string) Generator::create((createRsaKeys())->private),
+ 'accessTokenScope' => [uniqid(), 'read:admin'],
+ 'accessTokenExpiration' => time() + 60,
+ ])
+ ];
+});
+
+it('redirects to login route if a visitor does not have a session', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ $this->withoutExceptionHandling()
+ ->get($route)
+ ->assertRedirect('/login');
+
+ expect(redirect()->getIntendedUrl())
+ ->toEqual('http://localhost' . $route);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('assigns a user', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ $this->withSession($this->validSession)
+ ->get($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($route);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('assigns a user when using a configured scope matches', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate:read:admin')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ $this->withSession($this->validSession)
+ ->get($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($route);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('does not assign a user when a configured scope is not matched', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate:something:else')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ $this->withSession($this->validSession)
+ ->get($route)
+ ->assertStatus(Response::HTTP_FORBIDDEN);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('does not assign a user when an incompatible guard is used', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ config($config = [
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null
+ ]);
+
+ $this->get($route)
+ ->assertStatus(Response::HTTP_INTERNAL_SERVER_ERROR);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
diff --git a/tests/Unit/Middleware/AuthenticateOptionalMiddlewareTest.php b/tests/Unit/Middleware/AuthenticateOptionalMiddlewareTest.php
new file mode 100644
index 00000000..ea0393b0
--- /dev/null
+++ b/tests/Unit/Middleware/AuthenticateOptionalMiddlewareTest.php
@@ -0,0 +1,120 @@
+group('stateful', 'middleware', 'middleware.stateful', 'middleware.stateful.authenticate_optional');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $this->validSession = [
+ 'auth0_session' => json_encode([
+ 'user' => ['sub' => 'hello|world'],
+ 'idToken' => (string) Generator::create((createRsaKeys())->private),
+ 'accessToken' => (string) Generator::create((createRsaKeys())->private),
+ 'accessTokenScope' => [uniqid(), 'read:admin'],
+ 'accessTokenExpiration' => time() + 60,
+ ]),
+ ];
+});
+
+it('continues if a visitor does not have a session', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate.optional')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ $this->get($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($route);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('assigns a user', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate.optional')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ $this->withSession($this->validSession)
+ ->get($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($route);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('assigns a user when using a configured scope matches', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate.optional:read:admin')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ $this->withSession($this->validSession)
+ ->get($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($route);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('does not assign a user when a configured scope is not matched', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate.optional:something:else')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ $this->withSession($this->validSession)
+ ->get($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($route);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('does not assign a user when an incompatible guard is used', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate.optional')->get($route, function () use ($route): string {
+ return $route;
+ });
+
+ config($config = [
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null
+ ]);
+
+ $this->get($route)
+ ->assertStatus(Response::HTTP_INTERNAL_SERVER_ERROR);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
diff --git a/tests/Unit/Middleware/AuthenticatorMiddlewareTest.php b/tests/Unit/Middleware/AuthenticatorMiddlewareTest.php
new file mode 100644
index 00000000..769b2c22
--- /dev/null
+++ b/tests/Unit/Middleware/AuthenticatorMiddlewareTest.php
@@ -0,0 +1,39 @@
+group('Middleware/AuthenticatorMiddleware');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_NONE,
+ ]);
+});
+
+it('installs the Auth0 Authenticator', function (): void {
+ config(['auth.defaults.guard' => 'web']);
+ app(ServiceProvider::class, ['app' => app()])->registerMiddleware(app('router'));
+
+ Route::middleware('web')->get('/test', function (): JsonResponse {
+ if (auth()->guard()->name !== 'auth0-session') {
+ abort(Response::HTTP_INTERNAL_SERVER_ERROR);
+ }
+
+ return response()->json([
+ 'guard' => auth()->guard()->name,
+ 'middleware' => app('router')->getMiddlewareGroups()
+ ]);
+ });
+
+ $this->get('/test')
+ ->assertOK();
+});
diff --git a/tests/Unit/Middleware/AuthorizeMiddlewareTest.php b/tests/Unit/Middleware/AuthorizeMiddlewareTest.php
new file mode 100644
index 00000000..90ff54a6
--- /dev/null
+++ b/tests/Unit/Middleware/AuthorizeMiddlewareTest.php
@@ -0,0 +1,151 @@
+group('stateful', 'middleware', 'middleware.stateless', 'middleware.stateless.authorize');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_API,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.audience' => [uniqid()],
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+});
+
+it('does not assign a user when an incompatible guard is used', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ config([
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null
+ ]);
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . uniqid()])
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route]);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('returns a 401 and does not assign a user when an invalid bearer token is provided', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . uniqid()])
+ ->assertStatus(Response::HTTP_UNAUTHORIZED);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('assigns a user', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ $token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ "sub" => "auth0|123456",
+ "aud" => [
+ "https://example.com/health-api",
+ "https://my-domain.auth0.com/userinfo",
+ config('auth0.guards.default.clientId')
+ ],
+ "azp" => config('auth0.guards.default.clientId'),
+ "exp" => time() + 60,
+ "iat" => time(),
+ "scope" => "openid profile read:patients read:admin"
+ ]);
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . $token->toString()])
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('assigns a user when using a configured scope matches', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize:read:admin')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ $token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ "sub" => "auth0|123456",
+ "aud" => [
+ "https://example.com/health-api",
+ "https://my-domain.auth0.com/userinfo",
+ config('auth0.guards.default.clientId')
+ ],
+ "azp" => config('auth0.guards.default.clientId'),
+ "exp" => time() + 60,
+ "iat" => time(),
+ "scope" => "openid profile read:patients read:admin"
+ ]);
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . $token->toString()])
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('returns a 403 and does not assign a user when a configured scope is not matched', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize:something:else')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ $token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ "sub" => "auth0|123456",
+ "aud" => [
+ "https://example.com/health-api",
+ "https://my-domain.auth0.com/userinfo",
+ config('auth0.guards.default.clientId')
+ ],
+ "azp" => config('auth0.guards.default.clientId'),
+ "exp" => time() + 60,
+ "iat" => time(),
+ "scope" => "openid profile read:patients read:admin"
+ ]);
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . $token->toString()])
+ ->assertStatus(Response::HTTP_FORBIDDEN);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
diff --git a/tests/Unit/Middleware/AuthorizeOptionalMiddlewareTest.php b/tests/Unit/Middleware/AuthorizeOptionalMiddlewareTest.php
new file mode 100644
index 00000000..445901e3
--- /dev/null
+++ b/tests/Unit/Middleware/AuthorizeOptionalMiddlewareTest.php
@@ -0,0 +1,153 @@
+group('stateful', 'middleware', 'middleware.stateless', 'middleware.stateless.authorize');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_API,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.audience' => [uniqid()],
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+});
+
+it('does not assign a user when an invalid bearer token is provided', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize.optional')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . uniqid()])
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route]);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('assigns a user', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize.optional')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ $token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ "sub" => "auth0|123456",
+ "aud" => [
+ "https://example.com/health-api",
+ "https://my-domain.auth0.com/userinfo",
+ config('auth0.guards.default.clientId')
+ ],
+ "azp" => config('auth0.guards.default.clientId'),
+ "exp" => time() + 60,
+ "iat" => time(),
+ "scope" => "openid profile read:patients read:admin"
+ ]);
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . $token->toString()])
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('assigns a user when using a configured scope matches', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize.optional:read:admin')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ $token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ "sub" => "auth0|123456",
+ "aud" => [
+ "https://example.com/health-api",
+ "https://my-domain.auth0.com/userinfo",
+ config('auth0.guards.default.clientId')
+ ],
+ "azp" => config('auth0.guards.default.clientId'),
+ "exp" => time() + 60,
+ "iat" => time(),
+ "scope" => "openid profile read:patients read:admin"
+ ]);
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . $token->toString()])
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('does not assign a user when a configured scope is not matched', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize.optional:something:else')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ $token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ "sub" => "auth0|123456",
+ "aud" => [
+ "https://example.com/health-api",
+ "https://my-domain.auth0.com/userinfo",
+ config('auth0.guards.default.clientId')
+ ],
+ "azp" => config('auth0.guards.default.clientId'),
+ "exp" => time() + 60,
+ "iat" => time(),
+ "scope" => "openid profile read:patients read:admin"
+ ]);
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . $token->toString()])
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route]);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('does not assign a user when an incompatible guard is used', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize.optional')->get($route, function () use ($route): string {
+ return json_encode(['status' => $route]);
+ });
+
+ config([
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null
+ ]);
+
+ $this->getJson($route, ['Authorization' => 'Bearer ' . uniqid()])
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route]);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
diff --git a/tests/Unit/Middleware/AuthorizerMiddlewareTest.php b/tests/Unit/Middleware/AuthorizerMiddlewareTest.php
new file mode 100644
index 00000000..793fed12
--- /dev/null
+++ b/tests/Unit/Middleware/AuthorizerMiddlewareTest.php
@@ -0,0 +1,39 @@
+group('Middleware/AuthenticatorMiddleware');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_NONE,
+ ]);
+});
+
+it('installs the Auth0 Authenticator', function (): void {
+ config(['auth.defaults.guard' => 'web']);
+ app(ServiceProvider::class, ['app' => app()])->registerMiddleware(app('router'));
+
+ Route::middleware('api')->get('/test', function (): JsonResponse {
+ if (auth()->guard()->name !== 'auth0-api') {
+ abort(Response::HTTP_INTERNAL_SERVER_ERROR);
+ }
+
+ return response()->json([
+ 'guard' => auth()->guard()->name,
+ 'middleware' => app('router')->getMiddlewareGroups()
+ ]);
+ });
+
+ $this->get('/test')
+ ->assertOK();
+});
diff --git a/tests/Unit/Middleware/GuardMiddlewareTest.php b/tests/Unit/Middleware/GuardMiddlewareTest.php
new file mode 100644
index 00000000..170c5c2a
--- /dev/null
+++ b/tests/Unit/Middleware/GuardMiddlewareTest.php
@@ -0,0 +1,87 @@
+group('middleware', 'middleware.guard');
+
+beforeEach(function (): void {
+ $this->laravel = app('auth0');
+});
+
+it('assigns the guard for route handling', function (): void {
+ $routeMiddlewareAssignedGuard = '/' . uniqid();
+ $routeMiddlewareUnassignedGuard = '/' . uniqid();
+ $routeUnspecifiedGuard = '/' . uniqid();
+
+ $defaultGuardClass = 'Illuminate\Auth\SessionGuard';
+ $sdkGuardClass = 'Auth0\Laravel\Auth\Guard';
+
+ config(['auth.defaults.guard' => 'web']);
+
+ Route::get($routeUnspecifiedGuard, function (): string {
+ return get_class(auth()->guard());
+ });
+
+ Route::middleware('guard:legacyGuard')->get($routeMiddlewareAssignedGuard, function (): string {
+ return get_class(auth()->guard());
+ });
+
+ Route::middleware('guard')->get($routeMiddlewareUnassignedGuard, function (): string {
+ return get_class(auth()->guard());
+ });
+
+ $this->get($routeUnspecifiedGuard)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($defaultGuardClass);
+
+ $this->get($routeMiddlewareAssignedGuard)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($sdkGuardClass);
+
+ $this->get($routeUnspecifiedGuard)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($sdkGuardClass);
+
+ $this->get($routeMiddlewareUnassignedGuard)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($defaultGuardClass);
+
+ $this->get($routeUnspecifiedGuard)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($defaultGuardClass);
+});
+
+it('assigns the guard for route group handling', function (): void {
+ $routeMiddlewareUnassignedGuard = '/' . uniqid();
+ $routeUnspecifiedGuard = '/' . uniqid();
+
+ $defaultGuardClass = 'Illuminate\Auth\SessionGuard';
+ $sdkGuardClass = 'Auth0\Laravel\Auth\Guard';
+
+ config(['auth.defaults.guard' => 'web']);
+
+ Route::middleware('guard:legacyGuard')->group(function () use ($routeUnspecifiedGuard, $routeMiddlewareUnassignedGuard) {
+ Route::get($routeUnspecifiedGuard, function (): string {
+ return get_class(auth()->guard());
+ });
+
+ Route::middleware('guard')->get($routeMiddlewareUnassignedGuard, function (): string {
+ return get_class(auth()->guard());
+ });
+ });
+
+ $this->get($routeUnspecifiedGuard)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($sdkGuardClass);
+
+ $this->get($routeMiddlewareUnassignedGuard)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($defaultGuardClass);
+
+ $this->get($routeUnspecifiedGuard)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertSee($sdkGuardClass);
+});
diff --git a/tests/Unit/ServiceProviderTest.php b/tests/Unit/ServiceProviderTest.php
new file mode 100644
index 00000000..3ed809be
--- /dev/null
+++ b/tests/Unit/ServiceProviderTest.php
@@ -0,0 +1,552 @@
+group('ServiceProvider');
+
+function setupGuardImpersonation(
+ array $profile = [],
+ array $scope = [],
+ array $permissions = [],
+): Authenticatable {
+ Auth::shouldUse('legacyGuard');
+
+ $imposter = new ImposterUser(array_merge([
+ 'sub' => uniqid(),
+ 'name' => uniqid(),
+ 'email' => uniqid() . '@example.com',
+ ], $profile));
+
+ Auth::guard()->setImpersonating(CredentialEntity::create(
+ user: $imposter,
+ idToken: (string) Generator::create((createRsaKeys())->private),
+ accessToken: (string) Generator::create((createRsaKeys())->private),
+ accessTokenScope: $scope,
+ accessTokenDecoded: [
+ 'permissions' => $permissions,
+ ],
+ ));
+
+ return $imposter;
+}
+
+function resetGuard(
+ ?Authenticatable $imposter = null,
+): void {
+ Auth::shouldUse('web');
+
+ if (null !== $imposter) {
+ Auth::setUser($imposter);
+ }
+
+ config([
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null,
+ ]);
+}
+
+it('provides the expected classes', function (): void {
+ $service = app(ServiceProvider::class, ['app' => $this->app]);
+
+ expect($service->provides())
+ ->toBe([
+ Auth0::class,
+ AuthenticateMiddleware::class,
+ AuthenticateOptionalMiddleware::class,
+ AuthenticationGuard::class,
+ AuthenticatorMiddleware::class,
+ AuthorizationGuard::class,
+ AuthorizeMiddleware::class,
+ AuthorizeOptionalMiddleware::class,
+ AuthorizerMiddleware::class,
+ CacheBridge::class,
+ CacheItemBridge::class,
+ CallbackController::class,
+ Configuration::class,
+ Guard::class,
+ GuardMiddleware::class,
+ LoginController::class,
+ LogoutController::class,
+ Service::class,
+ SessionBridge::class,
+ UserProvider::class,
+ UserRepository::class,
+ ]);
+});
+
+it('creates a Service singleton with `auth0` alias', function (): void {
+ $singleton1 = $this->app->make('auth0');
+ $singleton2 = $this->app->make(Service::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(Service::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(Service::class);
+
+ expect($singleton1)
+ ->toBe($singleton2);
+});
+
+it('does NOT create a Guard singleton', function (): void {
+ $singleton1 = auth()->guard('legacyGuard');
+ $singleton2 = $this->app->make(Guard::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(Guard::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(Guard::class);
+
+ expect($singleton1)
+ ->not()->toBe($singleton2);
+});
+
+it('creates a UserRepository singleton', function (): void {
+ $singleton1 = $this->app->make('auth0.repository');
+ $singleton2 = $this->app->make(UserRepository::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(UserRepository::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(UserRepository::class);
+
+ expect($singleton1)
+ ->toBe($singleton2);
+});
+
+it('does NOT a Provider singleton', function (): void {
+ $singleton1 = Auth::createUserProvider('auth0-provider');
+ $singleton2 = $this->app->make(UserProvider::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(UserProvider::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(UserProvider::class);
+
+ expect($singleton1)
+ ->not()->toBe($singleton2);
+});
+
+it('creates a AuthenticateMiddleware singleton', function (): void {
+ $singleton1 = $this->app->make(AuthenticateMiddleware::class);
+ $singleton2 = $this->app->make(AuthenticateMiddleware::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(AuthenticateMiddleware::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(AuthenticateMiddleware::class);
+
+ expect($singleton1)
+ ->toBe($singleton2);
+});
+
+it('creates a AuthenticateOptionalMiddleware singleton', function (): void {
+ $singleton1 = $this->app->make(AuthenticateOptionalMiddleware::class);
+ $singleton2 = $this->app->make(AuthenticateOptionalMiddleware::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(AuthenticateOptionalMiddleware::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(AuthenticateOptionalMiddleware::class);
+
+ expect($singleton1)
+ ->toBe($singleton2);
+});
+
+it('creates a AuthorizeMiddleware singleton', function (): void {
+ $singleton1 = $this->app->make(AuthorizeMiddleware::class);
+ $singleton2 = $this->app->make(AuthorizeMiddleware::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(AuthorizeMiddleware::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(AuthorizeMiddleware::class);
+
+ expect($singleton1)
+ ->toBe($singleton2);
+});
+
+it('creates a AuthorizeOptionalMiddleware singleton', function (): void {
+ $singleton1 = $this->app->make(AuthorizeOptionalMiddleware::class);
+ $singleton2 = $this->app->make(AuthorizeOptionalMiddleware::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(AuthorizeOptionalMiddleware::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(AuthorizeOptionalMiddleware::class);
+
+ expect($singleton1)
+ ->toBe($singleton2);
+});
+
+it('creates a LoginController singleton', function (): void {
+ $singleton1 = $this->app->make(LoginController::class);
+ $singleton2 = $this->app->make(LoginController::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(LoginController::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(LoginController::class);
+
+ expect($singleton1)
+ ->toBe($singleton2);
+});
+
+it('creates a LogoutController singleton', function (): void {
+ $singleton1 = $this->app->make(LogoutController::class);
+ $singleton2 = $this->app->make(LogoutController::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(LogoutController::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(LogoutController::class);
+
+ expect($singleton1)
+ ->toBe($singleton2);
+});
+
+it('creates a CallbackController singleton', function (): void {
+ $singleton1 = $this->app->make(CallbackController::class);
+ $singleton2 = $this->app->make(CallbackController::class);
+
+ expect($singleton1)
+ ->toBeInstanceOf(CallbackController::class);
+
+ expect($singleton2)
+ ->toBeInstanceOf(CallbackController::class);
+
+ expect($singleton1)
+ ->toBe($singleton2);
+});
+
+test('Gate::check(`scope`) returns true when a match hits', function (): void {
+ setupGuardImpersonation(
+ scope: [uniqid(), 'testScope', uniqid()],
+ );
+
+ expect(Gate::check('scope', 'testScope'))
+ ->toBeTrue();
+});
+
+test('Gate::check(`scope`) returns false when a match misses', function (): void {
+ setupGuardImpersonation(
+ scope: [uniqid()],
+ );
+
+ expect(Gate::check('scope', 'testScope'))
+ ->toBeFalse();
+});
+
+test('Gate::check(`scope`) returns false when an incompatible Guard is used', function (): void {
+ $imposter = setupGuardImpersonation(
+ scope: [uniqid(), 'testScope', uniqid()],
+ );
+
+ resetGuard($imposter);
+
+ expect(Gate::check('scope', 'testScope'))
+ ->toBeFalse();
+});
+
+test('Gate::check(`permission`) returns true when a match hits', function (): void {
+ setupGuardImpersonation(
+ permissions: [uniqid(), 'testPermission', uniqid()],
+ );
+
+ expect(Gate::check('permission', 'testPermission'))
+ ->toBeTrue();
+});
+
+test('Gate::check(`permission`) returns false when a match misses', function (): void {
+ setupGuardImpersonation(
+ permissions: [uniqid()],
+ );
+
+ expect(Gate::check('permission', 'testPermission'))
+ ->toBeFalse();
+});
+
+test('Gate::check(`permission`) returns false when an incompatible Guard is used', function (): void {
+ $imposter = setupGuardImpersonation(
+ permissions: [uniqid(), 'testPermission', uniqid()],
+ );
+
+ resetGuard($imposter);
+
+ expect(Gate::check('permission', 'testPermission'))
+ ->toBeFalse();
+});
+
+test('policies `can(scope)` middleware returns true when a match hits', function (): void {
+ Route::get('/test', function () {
+ return response()->json(['status' => 'OK']);
+ })->can('scope:testScope');
+
+ setupGuardImpersonation(
+ scope: [uniqid(), 'testScope', uniqid()],
+ );
+
+ $this->getJson('/test')
+ ->assertOK();
+});
+
+test('policies `can(scope)` middleware returns false when a match misses', function (): void {
+ Route::get('/test', function () {
+ })->can('scope:testScope');
+
+ setupGuardImpersonation(
+ scope: [uniqid()],
+ );
+
+ $this->getJson('/test')
+ ->assertStatus(Response::HTTP_FORBIDDEN);
+});
+
+test('policies `can(scope)` middleware returns false when an incompatible Guard is used', function (): void {
+ Route::get('/test', function () {
+ return response()->json(['status' => 'OK']);
+ })->can('scope:testScope');
+
+ $imposter = setupGuardImpersonation(
+ scope: [uniqid(), 'testScope', uniqid()],
+ );
+
+ resetGuard($imposter);
+
+ $this->getJson('/test')
+ ->assertStatus(Response::HTTP_FORBIDDEN);
+});
+
+test('policies `can(permission)` middleware returns true when a match hits', function (): void {
+ Route::get('/test', function () {
+ return response()->json(['status' => 'OK']);
+ })->can('testing:123');
+
+ setupGuardImpersonation(
+ permissions: [uniqid(), 'testing:123', uniqid()],
+ );
+
+ $this->getJson('/test')
+ ->assertOK();
+});
+
+test('policies `can(permission)` middleware returns false when a match misses', function (): void {
+ Route::get('/test', function () {
+ return response()->json(['status' => 'OK']);
+ })->can('testing:123');
+
+ setupGuardImpersonation(
+ permissions: [uniqid(), 'testing:456', uniqid()],
+ );
+
+ $this->getJson('/test')
+ ->assertStatus(Response::HTTP_FORBIDDEN);
+});
+
+test('policies `can(permission)` middleware returns false when an incompatible Guard is used', function (): void {
+ Route::get('/test', function () {
+ return response()->json(['status' => 'OK']);
+ })->can('testing:123');
+
+ $imposter = setupGuardImpersonation(
+ permissions: [uniqid(), 'testing:123', uniqid()],
+ );
+
+ resetGuard($imposter);
+
+ $this->getJson('/test')
+ ->assertStatus(Response::HTTP_FORBIDDEN);
+});
+
+test('auth0.registerGuards === true registers guards', function (): void {
+ config(['auth0.registerGuards' => true]);
+
+ $service = app(ServiceProvider::class, ['app' => $this->app]);
+ /**
+ * @var ServiceProvider $service
+ */
+ $service->register();
+
+ expect(config('auth.guards.auth0-session'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'auth0.authenticator')
+ ->toHaveKey('configuration', 'web')
+ ->toHaveKey('provider', 'auth0-provider');
+
+ expect(config('auth.guards.auth0-api'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'auth0.authorizer')
+ ->toHaveKey('configuration', 'api')
+ ->toHaveKey('provider', 'auth0-provider');
+
+ expect(config('auth.providers.auth0-provider'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'auth0.provider')
+ ->toHaveKey('repository', 'auth0.repository');
+});
+
+test('auth0.registerGuards === true registers guards, but does not overwrite an existing auth.guards.auth0-session entry', function (): void {
+ config([
+ 'auth0.registerGuards' => true,
+ 'auth.guards.auth0-session' => [
+ 'driver' => 'session',
+ 'provider' => 'users',
+ ]
+ ]);
+
+ $service = app(ServiceProvider::class, ['app' => $this->app]);
+ /**
+ * @var ServiceProvider $service
+ */
+ $service->register();
+
+ expect(config('auth.guards.auth0-session'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'session')
+ ->toHaveKey('provider', 'users')
+ ->not()->toHaveKey('configuration');
+
+ expect(config('auth.guards.auth0-api'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'auth0.authorizer')
+ ->toHaveKey('configuration', 'api')
+ ->toHaveKey('provider', 'auth0-provider');
+
+ expect(config('auth.providers.auth0-provider'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'auth0.provider')
+ ->toHaveKey('repository', 'auth0.repository');
+});
+
+test('auth0.registerGuards === true registers guards, but does not overwrite an existing auth.guards.auth0-api entry', function (): void {
+ config([
+ 'auth0.registerGuards' => true,
+ 'auth.guards.auth0-api' => [
+ 'driver' => 'api',
+ 'provider' => 'users',
+ ]
+ ]);
+
+ $service = app(ServiceProvider::class, ['app' => $this->app]);
+ /**
+ * @var ServiceProvider $service
+ */
+ $service->register();
+
+ expect(config('auth.guards.auth0-session'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'auth0.authenticator')
+ ->toHaveKey('configuration', 'web')
+ ->toHaveKey('provider', 'auth0-provider');
+
+ expect(config('auth.guards.auth0-api'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'api')
+ ->toHaveKey('provider', 'users')
+ ->not()->toHaveKey('configuration');
+
+ expect(config('auth.providers.auth0-provider'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'auth0.provider')
+ ->toHaveKey('repository', 'auth0.repository');
+});
+
+test('auth0.registerGuards === true registers guards, but does not overwrite an existing auth.providers.auth0-provider entry', function (): void {
+ config([
+ 'auth0.registerGuards' => true,
+ 'auth.providers.auth0-provider' => [
+ 'driver' => 'database',
+ 'repository' => 'users',
+ ]
+ ]);
+
+ $service = app(ServiceProvider::class, ['app' => $this->app]);
+ /**
+ * @var ServiceProvider $service
+ */
+ $service->register();
+
+ expect(config('auth.guards.auth0-session'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'auth0.authenticator')
+ ->toHaveKey('configuration', 'web')
+ ->toHaveKey('provider', 'auth0-provider');
+
+ expect(config('auth.guards.auth0-api'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'auth0.authorizer')
+ ->toHaveKey('configuration', 'api')
+ ->toHaveKey('provider', 'auth0-provider');
+
+ expect(config('auth.providers.auth0-provider'))
+ ->toBeArray()
+ ->toHaveKey('driver', 'database')
+ ->toHaveKey('repository', 'users');
+});
+
+test('auth0.registerMiddleware === true registers middleware', function (): void {
+ config(['auth0.registerMiddleware' => true]);
+
+ $service = app(ServiceProvider::class, ['app' => $this->app]);
+ /**
+ * @var ServiceProvider $service
+ */
+ $service->registerMiddleware(app('router'));
+ /**
+ * @var \Illuminate\Foundation\Http\Kernel $kernel
+ */
+ $middleware = app('router')->getMiddlewareGroups();
+
+ expect($middleware)
+ ->toBeArray()
+ ->toHaveKeys(['web', 'api']);
+
+ expect($middleware['web'])
+ ->toContain(AuthenticatorMiddleware::class);
+
+ expect($middleware['api'])
+ ->toContain(AuthorizerMiddleware::class);
+});
+
+test('auth0.registerAuthenticationRoutes === true registers routes', function (): void {
+ config(['auth0.registerAuthenticationRoutes' => true]);
+
+ $service = app(ServiceProvider::class, ['app' => $this->app]);
+ /**
+ * @var ServiceProvider $service
+ */
+ $service->registerRoutes();
+ $routes = (array) Route::getRoutes()->get('GET');
+
+ expect($routes)
+ ->toHaveKeys(['login', 'logout', 'callback']);
+});
diff --git a/tests/Unit/ServiceTest.php b/tests/Unit/ServiceTest.php
new file mode 100644
index 00000000..6b5e677e
--- /dev/null
+++ b/tests/Unit/ServiceTest.php
@@ -0,0 +1,264 @@
+group('Service');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+ $this->config = $this->sdk->configuration();
+ $this->session = $this->config->getSessionStorage();
+});
+
+it('returns a Management API class', function (): void {
+ expect($this->laravel->management())->toBeInstanceOf(ManagementInterface::class);
+});
+
+it('can get/set the configuration', function (): void {
+ expect($this->laravel->getConfiguration())->toBeInstanceOf(SdkConfiguration::class);
+
+ $configuration = new SdkConfiguration(['strategy' => 'none', 'domain' => uniqid() . '.auth0.test']);
+ $this->laravel->setConfiguration($configuration);
+ expect($this->laravel->getConfiguration())->toBe($configuration);
+
+ $domain = uniqid() . '.auth0.test';
+ $configuration->setDomain($domain);
+ expect($this->laravel->getConfiguration()->getDomain())->toBe($domain);
+
+ $configuration = new SdkConfiguration(['strategy' => 'none', 'domain' => uniqid() . '.auth0.test']);
+ $this->laravel->setConfiguration($configuration);
+ expect($this->laravel->getConfiguration())->toBe($configuration);
+
+ $sdk = $this->laravel->getSdk();
+ $configuration = new SdkConfiguration(['strategy' => 'none', 'domain' => uniqid() . '.auth0.test']);
+ $this->laravel->setConfiguration($configuration);
+ expect($this->laravel->getConfiguration())->toBe($configuration);
+ expect($sdk->configuration())->toBe($configuration);
+});
+
+it('can get the sdk credentials', function (): void {
+ expect($this->laravel->getCredentials())
+ ->toBeNull();
+
+ $this->session->set('user', ['sub' => 'hello|world']);
+ $this->session->set('idToken', (string) Generator::create((createRsaKeys())->private));
+ $this->session->set('accessToken', (string) Generator::create((createRsaKeys())->private));
+ $this->session->set('accessTokenScope', [uniqid()]);
+ $this->session->set('accessTokenExpiration', time() - 1000);
+
+ // As we manually set the session values, we need to refresh the SDK state to ensure it's in sync.
+ $this->sdk->refreshState();
+
+ expect($this->laravel->getCredentials())
+ ->toBeObject()
+ ->toHaveProperty('accessToken', $this->session->get('accessToken'))
+ ->toHaveProperty('accessTokenScope', $this->session->get('accessTokenScope'))
+ ->toHaveProperty('accessTokenExpiration', $this->session->get('accessTokenExpiration'))
+ ->toHaveProperty('idToken', $this->session->get('idToken'))
+ ->toHaveProperty('user', $this->session->get('user'));
+});
+
+it('can get/set the SDK', function (): void {
+ expect($this->laravel->getSdk())->toBeInstanceOf(SdkContract::class);
+
+ $sdk = new SDKAuth0(['strategy' => 'none']);
+ $this->laravel->setSdk($sdk);
+ expect($this->laravel->getSdk())->toBeInstanceOf(SdkContract::class);
+});
+
+it('can reset the internal static state', function (): void {
+ $cache = spl_object_id($this->laravel->getSdk());
+
+ unset($this->laravel); // Force the object to be destroyed. Static state will remain.
+
+ $laravel = app('auth0');
+ $updated = spl_object_id($laravel->getSdk());
+ expect($cache)->toBe($updated);
+
+ $laravel->reset(); // Reset the static state.
+
+ $laravel = app('auth0');
+ $updated = spl_object_id($laravel->getSdk());
+ expect($cache)->not->toBe($updated);
+});
+
+test('bootStrategy() rejects non-string values', function (): void {
+ $method = new ReflectionMethod(Service::class, 'bootStrategy');
+ $method->setAccessible(true);
+
+ expect($method->invoke($this->laravel, ['strategy' => 123]))
+ ->toMatchArray(['strategy' => SdkConfiguration::STRATEGY_REGULAR]);
+});
+
+test('bootSessionStorage() behaves as expected', function (): void {
+ $method = new ReflectionMethod(Service::class, 'bootSessionStorage');
+ $method->setAccessible(true);
+
+ expect($method->invoke($this->laravel, []))
+ ->sessionStorage->toBeInstanceOf(SessionBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['sessionStorage' => null]))
+ ->sessionStorage->toBeInstanceOf(SessionBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['sessionStorage' => false]))
+ ->sessionStorage->toBeNull();
+
+ expect($method->invoke($this->laravel, ['sessionStorage' => CacheBridge::class]))
+ ->sessionStorage->toBeNull();
+
+ expect($method->invoke($this->laravel, ['sessionStorage' => MemoryStore::class]))
+ ->sessionStorage->toBeInstanceOf(MemoryStore::class);
+
+ $this->app->singleton('testStore', static fn (): MemoryStore => app(MemoryStore::class));
+
+ expect($method->invoke($this->laravel, ['sessionStorage' => 'testStore']))
+ ->sessionStorage->toBeInstanceOf(MemoryStore::class);
+});
+
+test('bootTransientStorage() behaves as expected', function (): void {
+ $method = new ReflectionMethod(Service::class, 'bootTransientStorage');
+ $method->setAccessible(true);
+
+ expect($method->invoke($this->laravel, []))
+ ->transientStorage->toBeInstanceOf(SessionBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['transientStorage' => null]))
+ ->transientStorage->toBeInstanceOf(SessionBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['transientStorage' => false]))
+ ->transientStorage->toBeNull();
+
+ expect($method->invoke($this->laravel, ['transientStorage' => CacheBridge::class]))
+ ->transientStorage->toBeNull();
+
+ expect($method->invoke($this->laravel, ['transientStorage' => MemoryStore::class]))
+ ->transientStorage->toBeInstanceOf(MemoryStore::class);
+
+ $this->app->singleton('testStore', static fn (): MemoryStore => app(MemoryStore::class));
+
+ expect($method->invoke($this->laravel, ['transientStorage' => 'testStore']))
+ ->transientStorage->toBeInstanceOf(MemoryStore::class);
+});
+
+test('bootTokenCache() behaves as expected', function (): void {
+ $method = new ReflectionMethod(Service::class, 'bootTokenCache');
+ $method->setAccessible(true);
+
+ expect($method->invoke($this->laravel, []))
+ ->tokenCache->toBeInstanceOf(CacheBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['tokenCache' => null]))
+ ->tokenCache->toBeInstanceOf(CacheBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['tokenCache' => CacheBridge::class]))
+ ->tokenCache->toBeInstanceOf(CacheBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['tokenCache' => false]))
+ ->tokenCache->toBeNull();
+
+ expect($method->invoke($this->laravel, ['tokenCache' => MemoryStore::class]))
+ ->tokenCache->toBeNull();
+
+ expect($method->invoke($this->laravel, ['tokenCache' => 'cache.psr6']))
+ ->tokenCache->toBeInstanceOf(CacheItemPoolInterface::class);
+});
+
+test('bootBackchannelLogoutCache() behaves as expected', function (): void {
+ $method = new ReflectionMethod(Service::class, 'bootBackchannelLogoutCache');
+ $method->setAccessible(true);
+
+ expect($method->invoke($this->laravel, []))
+ ->backchannelLogoutCache->toBeInstanceOf(CacheBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['backchannelLogoutCache' => null]))
+ ->backchannelLogoutCache->toBeInstanceOf(CacheBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['backchannelLogoutCache' => CacheBridge::class]))
+ ->backchannelLogoutCache->toBeInstanceOf(CacheBridgeContract::class);
+
+ expect($method->invoke($this->laravel, ['backchannelLogoutCache' => false]))
+ ->backchannelLogoutCache->toBeNull();
+
+ expect($method->invoke($this->laravel, ['backchannelLogoutCache' => MemoryStore::class]))
+ ->backchannelLogoutCache->toBeNull();
+
+ expect($method->invoke($this->laravel, ['backchannelLogoutCache' => 'cache.psr6']))
+ ->backchannelLogoutCache->toBeInstanceOf(CacheItemPoolInterface::class);
+});
+
+// test('bootManagementTokenCache() behaves as expected', function (): void {
+// $method = new ReflectionMethod(Service::class, 'bootManagementTokenCache');
+// $method->setAccessible(true);
+
+// expect($method->invoke($this->laravel, []))
+// ->managementTokenCache->toBeInstanceOf(CacheBridgeContract::class);
+
+// expect($method->invoke($this->laravel, ['managementTokenCache' => null]))
+// ->managementTokenCache->toBeInstanceOf(CacheBridgeContract::class);
+
+// expect($method->invoke($this->laravel, ['managementTokenCache' => CacheBridgeContract::class]))
+// ->managementTokenCache->toBeInstanceOf(CacheBridgeContract::class);
+
+// expect($method->invoke($this->laravel, ['managementTokenCache' => false]))
+// ->managementTokenCache->toBeNull();
+
+// expect($method->invoke($this->laravel, ['managementTokenCache' => MemoryStore::class]))
+// ->managementTokenCache->toBeNull();
+
+// expect($method->invoke($this->laravel, ['managementTokenCache' => 'cache.psr6']))
+// ->managementTokenCache->toBeInstanceOf(CacheItemPoolInterface::class);
+// });
+
+test('json() behaves as expected', function (): void {
+ $factory = new ResponseFactory;
+
+ $response = $factory->createResponse(200);
+ $response->getBody()->write('{"foo":"bar"}');
+
+ expect(Service::json($response))
+ ->toBe(['foo' => 'bar']);
+
+ $response = $factory->createResponse(500);
+ $response->getBody()->write('{"foo":"bar"}');
+
+ expect(Service::json($response))
+ ->toBeNull();
+
+ $response = $factory->createResponse(200);
+ $response->getBody()->write(json_encode(true));
+
+ expect(Service::json($response))
+ ->toBeNull();
+});
+
+test('routes() behaves as expected', function (): void {
+ Service::routes();
+
+ expect((array) Route::getRoutes()->get('GET'))
+ ->toHaveKeys(['login', 'logout', 'callback']);
+});
diff --git a/tests/Unit/Traits/ActingAsAuth0UserTest.php b/tests/Unit/Traits/ActingAsAuth0UserTest.php
new file mode 100644
index 00000000..c7c516f9
--- /dev/null
+++ b/tests/Unit/Traits/ActingAsAuth0UserTest.php
@@ -0,0 +1,251 @@
+group('trait', 'impersonation', 'acting-as-auth0-user');
+
+uses(ActingAsAuth0User::class);
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+ $this->user = ['sub' => uniqid(), 'scope' => 'openid profile email read:messages'];
+});
+
+it('impersonates with other guards', function (): void {
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null
+ ]);
+
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize')->get($route, function () use ($route): string {
+ return json_encode(['user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $this->actingAsAuth0User(attributes: $this->user, source: Guard::SOURCE_SESSION)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect(auth()->guard()->user())->not()->toBeNull();
+});
+
+it('impersonates a user against auth0.authenticate', function (): void {
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate')->get($route, function () use ($route) {
+ return response()->json([
+ 'user' => auth()->user(),
+ 'status' => $route
+ ]);
+ });
+
+ $this->actingAsAuth0User(attributes: $this->user, source: Guard::SOURCE_SESSION)
+ ->withoutExceptionHandling()
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJsonFragment(['sub' => $this->user['sub']]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('impersonates a user against auth0.authenticate.optional', function (): void {
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate.optional')->get($route, function () use ($route) {
+ return response()->json([
+ 'user' => auth()->user(),
+ 'status' => $route
+ ]);
+ });
+
+ $this->actingAsAuth0User(attributes: $this->user, source: Guard::SOURCE_SESSION)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJsonFragment(['sub' => $this->user['sub']]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('impersonates a user against auth0.authenticate using a scope', function (): void {
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate:read:messages')->get($route, function () use ($route) {
+ return response()->json([
+ 'user' => auth()->user(),
+ 'status' => $route
+ ]);
+ });
+
+ $this->actingAsAuth0User(attributes: $this->user, source: Guard::SOURCE_SESSION)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJsonFragment(['sub' => $this->user['sub']]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('impersonates a user against auth0.authorize', function (): void {
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_API,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.audience' => [uniqid()],
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize')->get($route, function () use ($route) {
+ return response()->json([
+ 'user' => auth()->user(),
+ 'status' => $route
+ ]);
+ });
+
+ $this->actingAsAuth0User(attributes: $this->user, source: Guard::SOURCE_TOKEN)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJsonFragment(['sub' => $this->user['sub']]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('impersonates a user against auth0.authorize.optional', function (): void {
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_API,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.audience' => [uniqid()],
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize.optional')->get($route, function () use ($route) {
+ return response()->json([
+ 'user' => auth()->user(),
+ 'status' => $route
+ ]);
+ });
+
+ $this->actingAsAuth0User(attributes: $this->user, source: Guard::SOURCE_TOKEN)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJsonFragment(['sub' => $this->user['sub']]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
+
+it('impersonates a user against auth0.authorize using a scope', function (): void {
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_API,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.audience' => [uniqid()],
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize:read:messages')->get($route, function () use ($route) {
+ return response()->json([
+ 'user' => auth()->user(),
+ 'status' => $route
+ ]);
+ });
+
+ $this->actingAsAuth0User(attributes: $this->user, source: Guard::SOURCE_TOKEN)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJsonFragment(['sub' => $this->user['sub']]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(UserContract::class);
+});
diff --git a/tests/Unit/Traits/ImpersonateTest.php b/tests/Unit/Traits/ImpersonateTest.php
new file mode 100644
index 00000000..830d94be
--- /dev/null
+++ b/tests/Unit/Traits/ImpersonateTest.php
@@ -0,0 +1,443 @@
+group('trait', 'impersonate');
+
+uses(Impersonate::class);
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_API,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.audience' => [uniqid()],
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ 'auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256,
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+});
+
+it('impersonates with other guards', function (): void {
+ config([
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null
+ ]);
+
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize')->get($route, function () use ($route): string {
+ return json_encode(['user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = CredentialEntity::create(
+ user: new ImposterUser(['sub' => uniqid()]),
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->impersonate($imposter)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($this->guard)
+ ->user()->toBeNull();
+});
+
+it('impersonates a user against auth0.authenticate', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate')->get($route, function () use ($route): string {
+ return json_encode(['user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = CredentialEntity::create(
+ user: new ImposterUser(['sub' => uniqid()]),
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->impersonate($imposter, Guard::SOURCE_SESSION)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJson(['user' => json_encode($imposter->getUser())]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(ImposterUser::class)
+ ->toBe($imposter->getUser());
+});
+
+it('impersonates a user against auth0.authenticate.optional', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate.optional')->get($route, function () use ($route): string {
+ return json_encode(['user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = CredentialEntity::create(
+ user: new ImposterUser(['sub' => uniqid()]),
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->impersonate($imposter, Guard::SOURCE_SESSION)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJson(['user' => json_encode($imposter->getUser())]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(ImposterUser::class)
+ ->toBe($imposter->getUser());
+});
+
+it('impersonates a user against auth0.authenticate using a scope', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authenticate:read:messages')->get($route, function () use ($route): string {
+ return json_encode(['user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = CredentialEntity::create(
+ user: new ImposterUser(['sub' => uniqid()]),
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->impersonate($imposter, Guard::SOURCE_SESSION)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJson(['user' => json_encode($imposter->getUser())]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(ImposterUser::class)
+ ->toBe($imposter->getUser());
+});
+
+it('impersonates a user against auth0.authorize', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize')->get($route, function () use ($route): string {
+ return json_encode(['user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = CredentialEntity::create(
+ user: new ImposterUser(['sub' => uniqid()]),
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->impersonate($imposter, Guard::SOURCE_TOKEN)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJson(['user' => json_encode($imposter->getUser())]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(ImposterUser::class)
+ ->toBe($imposter->getUser());
+});
+
+it('impersonates a user against auth0.authorize.optional', function (): void {
+ $route = '/' . uniqid();
+
+ Route::middleware('auth0.authorize.optional')->get($route, function () use ($route): string {
+ return json_encode(['user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = CredentialEntity::create(
+ user: new ImposterUser(['sub' => uniqid()]),
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ $this->impersonate($imposter, Guard::SOURCE_TOKEN)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJson(['user' => json_encode($imposter->getUser())]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(ImposterUser::class)
+ ->toBe($imposter->getUser());
+});
+
+it('impersonates a user against auth0.authorize using a scope', function (): void {
+ $route = '/' . uniqid();
+
+ $imposter = CredentialEntity::create(
+ user: new ImposterUser(['sub' => uniqid()]),
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600
+ );
+
+ Route::middleware('auth0.authorize:read:messages')->get($route, function () use ($route): string {
+ return json_encode(['user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $this->impersonate($imposter, Guard::SOURCE_TOKEN)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK)
+ ->assertJson(['status' => $route])
+ ->assertJson(['user' => json_encode($imposter->getUser())]);
+
+ expect($this->guard)
+ ->user()->toBeInstanceOf(ImposterUser::class)
+ ->toBe($imposter->getUser());
+});
+
+it('AuthenticationGuard returns the impersonated user', function (): void {
+ config([
+ 'auth.defaults.guard' => 'auth0-session',
+ ]);
+
+ $route = '/' . uniqid();
+
+ Route::get($route, function () use ($route): string {
+ return json_encode(['route' => get_class(auth()->guard()), 'user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = new ImposterUser(['sub' => uniqid()]);
+
+ $credential = CredentialEntity::create(
+ user: $imposter,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600,
+ refreshToken: uniqid(),
+ );
+
+ $response = $this->impersonate($credential)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($response->json())
+ ->route->toBe(AuthenticationGuard::class)
+ ->user->json()->sub->toBe($imposter->getAuthIdentifier());
+
+ expect(auth('legacyGuard'))
+ ->user()->toBeNull();
+
+ expect(auth('auth0-api'))
+ ->user()->toBeNull();
+
+ expect(auth('auth0-session'))
+ ->isImpersonating()->toBeTrue()
+ ->user()->toEqual($imposter)
+ ->find()->toEqual($credential)
+ ->findSession()->toEqual($credential)
+ ->getCredential()->toEqual($credential);
+
+ $client = new MockHttpClient(requestLimit: 0);
+ $this->sdk->configuration()->setHttpClient($client);
+
+ expect(auth('auth0-session'))
+ ->refreshUser();
+
+ auth('auth0-session')->setUser(new ImposterUser(['sub' => uniqid()]));
+
+ expect(auth('auth0-session'))
+ ->isImpersonating()->toBeFalse()
+ ->user()->not()->toEqual($imposter)
+ ->find()->not()->toEqual($credential)
+ ->findSession()->not()->toEqual($credential)
+ ->getCredential()->not()->toEqual($credential);
+});
+
+it('AuthorizationGuard returns the impersonated user', function (): void {
+ config([
+ 'auth.defaults.guard' => 'auth0-api',
+ ]);
+
+ $route = '/' . uniqid();
+
+ Route::get($route, function () use ($route): string {
+ return json_encode(['route' => get_class(auth()->guard()), 'user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = new ImposterUser(['sub' => uniqid()]);
+
+ $credential = CredentialEntity::create(
+ user: $imposter,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600,
+ refreshToken: uniqid(),
+ );
+
+ $response = $this->impersonate($credential)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($response->json())
+ ->route->toBe(AuthorizationGuard::class)
+ ->user->json()->sub->toBe($imposter->getAuthIdentifier());
+
+ expect(auth('legacyGuard'))
+ ->user()->toBeNull();
+
+ expect(auth('auth0-session'))
+ ->user()->toBeNull();
+
+ expect(auth('auth0-api'))
+ ->isImpersonating()->toBeTrue()
+ ->getImposterSource()->toBe(Guard::SOURCE_TOKEN)
+ ->user()->toEqual($imposter)
+ ->find()->toEqual($credential)
+ ->findToken()->toEqual($credential)
+ ->getCredential()->toEqual($credential);
+
+ $client = new MockHttpClient(requestLimit: 0);
+ $this->sdk->configuration()->setHttpClient($client);
+
+ expect(auth('auth0-api'))
+ ->refreshUser();
+
+ auth('auth0-api')->setUser(new ImposterUser(['sub' => uniqid()]));
+
+ expect(auth('auth0-api'))
+ ->isImpersonating()->toBeFalse()
+ ->user()->not()->toEqual($imposter)
+ ->find()->not()->toEqual($credential)
+ ->findToken()->not()->toEqual($credential)
+ ->getCredential()->not()->toEqual($credential);
+});
+
+it('Guard returns the impersonated user', function (): void {
+ config([
+ 'auth.defaults.guard' => 'legacyGuard',
+ ]);
+
+ $route = '/' . uniqid();
+
+ Route::get($route, function () use ($route): string {
+ return json_encode(['route' => get_class(auth()->guard()), 'user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = new ImposterUser(['sub' => uniqid()]);
+
+ $credential = CredentialEntity::create(
+ user: $imposter,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600,
+ refreshToken: uniqid(),
+ );
+
+ $response = $this->impersonate(credential: $credential, source: Guard::SOURCE_SESSION)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($response->json())
+ ->route->toBe(Guard::class)
+ ->user->json()->sub->toBe($imposter->getAuthIdentifier());
+
+ expect(auth('auth0-api'))
+ ->user()->toBeNull();
+
+ expect(auth('auth0-session'))
+ ->user()->toBeNull();
+
+ expect(auth('legacyGuard'))
+ ->isImpersonating()->toBeTrue()
+ ->getImposterSource()->toBe(Guard::SOURCE_SESSION)
+ ->user()->toEqual($imposter)
+ ->find()->toEqual($credential)
+ ->getCredential()->toEqual($credential);
+
+ $client = new MockHttpClient(requestLimit: 0);
+ $this->sdk->configuration()->setHttpClient($client);
+
+ auth('legacyGuard')->refreshUser();
+ auth('legacyGuard')->setUser(new ImposterUser(['sub' => uniqid()]));
+
+ expect(auth('legacyGuard'))
+ ->isImpersonating()->toBeFalse()
+ ->user()->not()->toEqual($imposter)
+ ->find()->not()->toEqual($credential)
+ ->getCredential()->not()->toEqual($credential);
+});
+
+it('Guard clears the impersonated user during logout()', function (): void {
+ config([
+ 'auth.defaults.guard' => 'legacyGuard',
+ ]);
+
+ $route = '/' . uniqid();
+
+ Route::get($route, function () use ($route): string {
+ return json_encode(['route' => get_class(auth()->guard()), 'user' => json_encode(auth()->user()), 'status' => $route]);
+ });
+
+ $imposter = new ImposterUser(['sub' => uniqid()]);
+
+ $credential = CredentialEntity::create(
+ user: $imposter,
+ idToken: mockIdToken(algorithm: Token::ALGO_HS256),
+ accessToken: mockAccessToken(algorithm: Token::ALGO_HS256),
+ accessTokenScope: ['openid', 'profile', 'email', 'read:messages'],
+ accessTokenExpiration: time() + 3600,
+ refreshToken: uniqid(),
+ );
+
+ $response = $this->impersonate(credential: $credential, source: Guard::SOURCE_TOKEN)
+ ->getJson($route)
+ ->assertStatus(Response::HTTP_OK);
+
+ expect($response->json())
+ ->route->toBe(Guard::class)
+ ->user->json()->sub->toBe($imposter->getAuthIdentifier());
+
+ expect(auth('legacyGuard'))
+ ->isImpersonating()->toBeTrue()
+ ->getImposterSource()->toBe(Guard::SOURCE_TOKEN)
+ ->user()->toEqual($imposter)
+ ->find()->toEqual($credential)
+ ->getCredential()->toEqual($credential);
+
+ auth('legacyGuard')->logout();
+
+ expect(auth('legacyGuard'))
+ ->isImpersonating()->toBeFalse()
+ ->user()->toBeNull()
+ ->find()->toBeNull()
+ ->getCredential()->toBeNull();
+});
diff --git a/tests/Unit/UserProviderTest.php b/tests/Unit/UserProviderTest.php
new file mode 100644
index 00000000..60d12d15
--- /dev/null
+++ b/tests/Unit/UserProviderTest.php
@@ -0,0 +1,166 @@
+group('UserProvider');
+
+beforeEach(function (): void {
+ $this->secret = uniqid();
+
+ config([
+ 'auth0.AUTH0_CONFIG_VERSION' => 2,
+ 'auth0.guards.default.strategy' => SdkConfiguration::STRATEGY_REGULAR,
+ 'auth0.guards.default.domain' => uniqid() . '.auth0.com',
+ 'auth0.guards.default.clientId' => uniqid(),
+ 'auth0.guards.default.clientSecret' => $this->secret,
+ 'auth0.guards.default.cookieSecret' => uniqid(),
+ ]);
+
+ $this->laravel = app('auth0');
+ $this->guard = auth('legacyGuard');
+ $this->sdk = $this->laravel->getSdk();
+});
+
+test('retrieveByToken() returns null when an incompatible guard token is used', function (): void {
+ config([
+ 'auth.defaults.guard' => 'web',
+ 'auth.guards.legacyGuard' => null
+ ]);
+
+ $route = '/' . uniqid();
+ Route::get($route, function () {
+ $provider = Auth::createUserProvider('auth0-provider');
+ $credential = $provider->retrieveByToken('token', '');
+
+ if (null === $credential) {
+ return response()->json(['status' => 'OK']);
+ }
+
+ abort(Response::HTTP_UNAUTHORIZED, 'Unauthorized');
+ });
+
+ $this->getJson($route)
+ ->assertOK();
+});
+
+test('retrieveByToken() returns null when an invalid token is provided', function (): void {
+ config(['auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256]);
+
+ $token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://123.' . config('auth0.guards.default.domain') . '/',
+ 'sub' => 'hello|world',
+ 'aud' => config('auth0.guards.default.clientId'),
+ 'iat' => time(),
+ 'exp' => time() + 60,
+ 'azp' => config('auth0.guards.default.clientId'),
+ 'scope' => 'openid profile email'
+ ], []);
+
+ $route = '/' . uniqid();
+ Route::get($route, function () use ($token) {
+ $provider = Auth::createUserProvider('auth0-provider');
+ $credential = $provider->retrieveByToken('token', $token);
+
+ if (null === $credential) {
+ return response()->json(['status' => 'OK']);
+ }
+
+ abort(Response::HTTP_UNAUTHORIZED, 'Unauthorized');
+ });
+
+
+ $this->getJson($route)
+ ->assertOK();
+});
+
+test('retrieveByToken() returns a user when a valid token is provided', function (): void {
+ config(['auth0.guards.default.tokenAlgorithm' => Token::ALGO_HS256]);
+
+ $token = Generator::create($this->secret, Token::ALGO_HS256, [
+ "iss" => 'https://' . config('auth0.guards.default.domain') . '/',
+ 'sub' => 'hello|world',
+ 'aud' => config('auth0.guards.default.clientId'),
+ 'iat' => time(),
+ 'exp' => time() + 60,
+ 'azp' => config('auth0.guards.default.clientId'),
+ 'scope' => 'openid profile email'
+ ], []);
+
+ $route = '/' . uniqid();
+ Route::get($route, function () use ($token) {
+ $provider = Auth::createUserProvider('auth0-provider');
+ $credential = $provider->retrieveByToken('token', (string) $token);
+
+ if (null !== $credential) {
+ return response()->json(['status' => 'OK']);
+ }
+
+ abort(Response::HTTP_UNAUTHORIZED, 'Unauthorized');
+ });
+
+ $this->getJson($route)
+ ->assertOK();
+});
+
+test('validateCredentials() always returns false', function (): void {
+ $provider = Auth::createUserProvider('auth0-provider');
+ $user = new StatefulUser();
+
+ expect($provider->validateCredentials($user, []))
+ ->toBeFalse();
+});
+
+test('getRepository() throws an error when an non-existent repository provider is set', function (): void {
+ $provider = new UserProvider(['model' => 'MISSING']);
+ $provider->getRepository();
+})->throws(BindingResolutionException::class);
+
+test('getRepository() throws an error when an invalid repository provider is set', function (): void {
+ $provider = new UserProvider(['model' => ['ARRAY']]);
+ $provider->getRepository();
+})->throws(BindingResolutionException::class);
+
+test('setRepository() sets the repository model', function (): void {
+ $provider = new UserProvider(['model' => uniqid()]);
+ $repository = new UserRepository();
+ $provider->setRepository($repository::class);
+
+ expect($provider->getRepository())
+ ->toBeInstanceOf($repository::class);
+});
+
+test('setRepository() with the same repository identifier uses the cached repository instance', function (): void {
+ $provider = new UserProvider(['model' => 'MISSING']);
+ $repository = new UserRepository();
+
+ $provider->setRepository($repository::class);
+
+ expect($provider->getRepository())
+ ->toBeInstanceOf($repository::class);
+
+ $provider->setRepository($repository::class);
+
+ expect($provider->getRepository())
+ ->toBeInstanceOf($repository::class);
+});
+
+test('retrieveByCredentials() returns `null` when an empty array is provided', function (): void {
+ $provider = new UserProvider(['model' => uniqid()]);
+ $repository = new UserRepository();
+
+ expect($provider->retrieveByCredentials([]))->toBeNull();
+});
diff --git a/tests/Unit/UserRepositoryTest.php b/tests/Unit/UserRepositoryTest.php
new file mode 100644
index 00000000..2c06e9af
--- /dev/null
+++ b/tests/Unit/UserRepositoryTest.php
@@ -0,0 +1,24 @@
+group('UserRepository');
+
+it('returns a stateful user model from session queries', function (): void {
+ $repository = $this->app['auth0.repository'];
+
+ expect($repository->fromSession(['name' => 'Stateful']))
+ ->toBeInstanceOf(StatefulUser::class)
+ ->name->toBe('Stateful');
+});
+
+it('returns a stateless user model from access token queries', function (): void {
+ $repository = $this->app['auth0.repository'];
+
+ expect($repository->fromAccessToken(['name' => 'Stateless']))
+ ->toBeInstanceOf(StatelessUser::class)
+ ->name->toBe('Stateless');
+});
diff --git a/tests/Unit/Users/UserTest.php b/tests/Unit/Users/UserTest.php
new file mode 100644
index 00000000..1bb58ecf
--- /dev/null
+++ b/tests/Unit/Users/UserTest.php
@@ -0,0 +1,110 @@
+group('stateful', 'model', 'model.user');
+
+it('fills attributes provided to the constructor', function (): void {
+ $user = new ImposterUser(['testing' => 'testing']);
+
+ expect($user->testing)
+ ->toBe('testing');
+});
+
+it('fills attributes', function (): void {
+ $user = new ImposterUser();
+ $user->fill(['testing' => 'testing']);
+
+ expect($user->testing)
+ ->toBe('testing');
+});
+
+it('sets attributes with magic', function (): void {
+ $user = new ImposterUser();
+ $user->testing = 'testing';
+
+ expect($user->testing)
+ ->toBe('testing');
+});
+
+it('sets attributes', function (): void {
+ $user = new ImposterUser();
+ $user->setAttribute('testing', 'testing');
+
+ expect($user->getAttribute('testing'))
+ ->toBe('testing');
+});
+
+it('gets attributes array', function (): void {
+ $user = new ImposterUser([
+ 'testing' => 'testing',
+ 'testing2' => 'testing2',
+ ]);
+
+ expect($user->getAttributes())
+ ->toBeArray()
+ ->toContain('testing')
+ ->toContain('testing2');
+});
+
+it('supports getting the identifier', function (): void {
+ $user = new ImposterUser(['sub' => 'testing']);
+
+ expect($user->getAuthIdentifier())
+ ->toBe('testing');
+});
+
+it('supports getting the identifier name', function (): void {
+ $user = new ImposterUser(['sub' => 'testing']);
+
+ expect($user->getAuthIdentifierName())
+ ->toBe('id');
+});
+
+it('supports getting the password', function (): void {
+ $user = new ImposterUser();
+
+ expect($user->getAuthPassword())
+ ->toBe('');
+});
+
+it('supports getting the password name', function (): void {
+ $user = new ImposterUser();
+
+ expect($user->getAuthPasswordName())
+ ->toBe('password');
+});
+
+it('supports getting the remember token', function (): void {
+ $user = new ImposterUser();
+
+ expect($user->getRememberToken())
+ ->toBe('');
+});
+
+it('supports getting the remember token name', function (): void {
+ $user = new ImposterUser();
+
+ expect($user->getRememberTokenName())
+ ->toBe('');
+});
+
+it('supports setting the remember token', function (): void {
+ $user = new ImposterUser();
+
+ expect($user->setRememberToken('testing'))
+ ->toBeNull();
+});
+
+it('supports JSON serialization', function (): void {
+ $user = new ImposterUser(['testing' => 'testing']);
+
+ expect($user->jsonSerialize())
+ ->toBeArray()
+ ->toContain('testing');
+
+ expect(json_encode($user))
+ ->toBeJson('{"testing":"testing"}');
+});
diff --git a/tests/constants.php b/tests/constants.php
deleted file mode 100644
index bf70fd4e..00000000
--- a/tests/constants.php
+++ /dev/null
@@ -1,19 +0,0 @@
-