├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── Bug Report.yml │ ├── Feature Request.yml │ └── config.yml └── workflows │ ├── cron_snyk.yml │ ├── pr_await_changes.yml │ ├── pr_composer.yml │ ├── pr_pest.yml │ ├── pr_phpcsf.yml │ ├── pr_phpstan.yml │ ├── pr_psalm.yml │ ├── pr_rector.yml │ ├── pr_snyk.yml │ └── semgrep.yml ├── .gitignore ├── LICENSE ├── README.md └── app ├── .editorconfig ├── .env.example ├── .php-cs-fixer.dist.php ├── .phpcs.xml.dist ├── README.md ├── composer.json ├── phpstan.neon.dist ├── phpunit.xml ├── psalm.xml.dist ├── public ├── bootstrap.php └── index.php ├── rector.php ├── src ├── Application.php ├── ApplicationErrorHandler.php ├── ApplicationRouter.php ├── ApplicationTemplates.php ├── Contract │ └── QuickstartExample.php └── Example │ ├── HttpEventListeners.php │ ├── Management.php │ └── PasswordlessMagic.php ├── templates ├── _layout.php ├── error.php ├── logged-in.php ├── logged-out.php ├── management.php ├── passwordless-magic-callback.php └── passwordless-magic-login.php └── tests ├── Feature └── ExampleTest.php ├── Pest.php ├── TestCase.php └── Unit └── ExampleTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | # We use 4 space indentation for PHP 10 | [*.php] 11 | indent_style = space 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @auth0-samples/dx-sdks-engineer 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug Report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Report a bug 2 | description: Have you found a bug or issue? Create a bug report for this sample 3 | 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Please do not report security vulnerabilities here**. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. 9 | 10 | - type: checkboxes 11 | id: checklist 12 | attributes: 13 | label: Checklist 14 | options: 15 | - label: I have looked into the [Readme](https://github.com/auth0-samples/auth0-php-web-app/tree/main/app#readme) and have not found a suitable solution or answer. 16 | required: true 17 | - label: I have searched the [issues](https://github.com/auth0-samples/auth0-php-web-app/issues) and have not found a suitable solution or answer. 18 | required: true 19 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 20 | required: true 21 | - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). 22 | required: true 23 | 24 | - type: textarea 25 | id: description 26 | attributes: 27 | label: Description 28 | description: Provide a clear and concise description of the issue, including what you expected to happen. 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: reproduction 34 | attributes: 35 | label: Reproduction 36 | description: Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. 37 | placeholder: | 38 | 1. Step 1... 39 | 2. Step 2... 40 | 3. ... 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: additional-context 46 | attributes: 47 | label: Additional context 48 | description: Any other relevant information you think would be useful. 49 | validations: 50 | required: false 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature Request.yml: -------------------------------------------------------------------------------- 1 | name: 🧩 Feature request 2 | description: Suggest an idea or a feature for this sample 3 | labels: ["feature request"] 4 | 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | options: 11 | - label: I have looked into the [Readme](https://github.com/auth0-samples/auth0-php-web-app/tree/main/app#readme) and have not found a suitable solution or answer. 12 | required: true 13 | - label: I have searched the [issues](https://github.com/auth0-samples/auth0-php-web-app/issues) and have not found a suitable solution or answer. 14 | required: true 15 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 16 | required: true 17 | - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). 18 | required: true 19 | 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: Describe the problem you'd like to have solved 24 | description: A clear and concise description of what the problem is. 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: ideal-solution 30 | attributes: 31 | label: Describe the ideal solution 32 | description: A clear and concise description of what you want to happen. 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: alternatives-and-workarounds 38 | attributes: 39 | label: Alternatives and current workarounds 40 | description: A clear and concise description of any alternatives you've considered or any workarounds that are currently in place. 41 | validations: 42 | required: false 43 | 44 | - type: textarea 45 | id: additional-context 46 | attributes: 47 | label: Additional context 48 | description: Add any other context or screenshots about the feature request here. 49 | validations: 50 | required: false 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🤔 Help & Questions 4 | url: https://community.auth0.com 5 | about: Ask general support or usage questions in the Auth0 Community forums. 6 | -------------------------------------------------------------------------------- /.github/workflows/cron_snyk.yml: -------------------------------------------------------------------------------- 1 | name: "Snyk (Scheduled)" 2 | 3 | # This workflow will run after a push to the main branch and as a scheduled job. 4 | 5 | on: 6 | push: 7 | branches: ["master", "main"] 8 | 9 | permissions: {} 10 | 11 | defaults: 12 | run: 13 | working-directory: app 14 | 15 | jobs: 16 | snyk: 17 | name: "Scan" 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: "8.1" 24 | coverage: none 25 | extensions: mbstring 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - uses: actions/checkout@v3 30 | 31 | - run: composer install --no-progress 32 | 33 | - uses: snyk/actions/php@master 34 | continue-on-error: true 35 | env: 36 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/pr_await_changes.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Changes" 2 | 3 | # Monitor for changes to pull requests. 4 | 5 | on: 6 | pull_request: 7 | types: [opened, synchronize, reopened, closed] 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | wait: 13 | name: "Watching" 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - run: echo "Child workflows triggered." 18 | -------------------------------------------------------------------------------- /.github/workflows/pr_composer.yml: -------------------------------------------------------------------------------- 1 | name: "Composer" 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | push: 7 | branches: ["master", "main"] 8 | 9 | permissions: {} 10 | 11 | defaults: 12 | run: 13 | working-directory: app 14 | 15 | jobs: 16 | validate: 17 | name: "Validate" 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - run: composer validate 24 | 25 | normalize: 26 | name: "Normalize" 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - run: composer require --dev ergebnis/composer-normalize 33 | 34 | - run: composer config allow-plugins.ergebnis/composer-normalize true 35 | 36 | - run: composer normalize 37 | -------------------------------------------------------------------------------- /.github/workflows/pr_pest.yml: -------------------------------------------------------------------------------- 1 | name: "PEST" 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | push: 7 | branches: ["master", "main"] 8 | 9 | permissions: {} 10 | 11 | defaults: 12 | run: 13 | working-directory: app 14 | 15 | jobs: 16 | pest: 17 | name: "Scan" 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "8.1" 26 | coverage: pcov 27 | 28 | - run: composer install --no-progress 29 | 30 | - run: vendor/bin/pest --order-by random --fail-on-risky --stop-on-defect --coverage --parallel 31 | 32 | - uses: codecov/codecov-action@v3 33 | with: 34 | directory: ./coverage/ 35 | flags: unittests 36 | -------------------------------------------------------------------------------- /.github/workflows/pr_phpcsf.yml: -------------------------------------------------------------------------------- 1 | name: "PHP CS Fixer" 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | push: 7 | branches: ["master", "main"] 8 | 9 | permissions: {} 10 | 11 | defaults: 12 | run: 13 | working-directory: app 14 | 15 | jobs: 16 | phpcsf: 17 | name: "Scan" 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "8.1" 26 | 27 | - run: composer install --no-progress 28 | 29 | - run: vendor/bin/php-cs-fixer fix src --dry-run --diff 30 | -------------------------------------------------------------------------------- /.github/workflows/pr_phpstan.yml: -------------------------------------------------------------------------------- 1 | name: "PHPStan" 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | push: 7 | branches: ["master", "main"] 8 | 9 | permissions: {} 10 | 11 | defaults: 12 | run: 13 | working-directory: app 14 | 15 | jobs: 16 | phpstan: 17 | name: "Scan" 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "8.1" 26 | 27 | - run: composer install --no-progress 28 | 29 | # - run: vendor/bin/phpstan analyze --no-ansi --no-progress --debug 30 | -------------------------------------------------------------------------------- /.github/workflows/pr_psalm.yml: -------------------------------------------------------------------------------- 1 | name: "Psalm" 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | push: 7 | branches: ["master", "main"] 8 | 9 | permissions: {} 10 | 11 | defaults: 12 | run: 13 | working-directory: app 14 | 15 | jobs: 16 | psalm: 17 | name: "Scan" 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "8.1" 26 | 27 | - run: composer install --no-progress 28 | 29 | # - run: vendor/bin/psalm 30 | -------------------------------------------------------------------------------- /.github/workflows/pr_rector.yml: -------------------------------------------------------------------------------- 1 | name: "Rector" 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | push: 7 | branches: ["master", "main"] 8 | 9 | permissions: {} 10 | 11 | defaults: 12 | run: 13 | working-directory: app 14 | 15 | jobs: 16 | rector: 17 | name: "Scan" 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "8.1" 26 | 27 | - run: composer install --no-progress 28 | 29 | - run: vendor/bin/rector process --dry-run 30 | -------------------------------------------------------------------------------- /.github/workflows/pr_snyk.yml: -------------------------------------------------------------------------------- 1 | name: "Snyk" 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Pull Request Changes"] 6 | types: 7 | - completed 8 | 9 | permissions: {} 10 | 11 | defaults: 12 | run: 13 | working-directory: app 14 | 15 | jobs: 16 | snyk: 17 | name: "Scan" 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: "8.1" 24 | coverage: none 25 | extensions: mbstring 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - uses: actions/checkout@v3 30 | with: 31 | ref: ${{ github.event.pull_request.head.sha }} 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - run: composer install --no-progress 35 | 36 | - uses: snyk/actions/php@master 37 | continue-on-error: true 38 | env: 39 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | pull_request_target: {} 5 | push: 6 | branches: ["master", "main"] 7 | permissions: 8 | contents: read 9 | jobs: 10 | semgrep: 11 | name: Scan 12 | runs-on: ubuntu-latest 13 | container: 14 | image: returntocorp/semgrep 15 | if: (github.actor != 'dependabot[bot]' && github.actor != 'snyk-bot') 16 | steps: 17 | - uses: actions/checkout@v3 18 | - run: semgrep ci 19 | env: 20 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/vendor 2 | app/.env 3 | app/composer.lock 4 | .DS_Store 5 | app/.php-cs-fixer.cache 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Auth0 Samples 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auth0 PHP Web App Sample 2 | 3 | This sample demonstrates how to add authorization to a [PHP](http://php.net/) web app using [Auth0](https://auth0.com). The project source code can be found in the [app directory](./app). Check the [PHP Quickstart](https://auth0.com/docs/quickstart/webapp/php) to understand this sample better. 4 | 5 | ## Vulnerability Reporting 6 | 7 | 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. 8 | 9 | ## What is Auth0? 10 | 11 | Auth0 helps you to easily: 12 | 13 | - implement authentication with multiple identity providers, including social (e.g., Google, Facebook, Microsoft, LinkedIn, GitHub, Twitter, etc), or enterprise (e.g., Windows Azure AD, Google Apps, Active Directory, ADFS, SAML, etc.) 14 | - log in users with username/password databases, passwordless, or multi-factor authentication 15 | - link multiple user accounts together 16 | - generate signed JSON Web Tokens to authorize your API calls and flow the user identity securely 17 | - access demographics and analytics detailing how, when, and where users are logging in 18 | - enrich user profiles from other data sources using customizable JavaScript rules 19 | 20 | [Why Auth0?](https://auth0.com/why-auth0) 21 | 22 | ## License 23 | 24 | This project is licensed under the MIT license. See the [LICENSE](https://github.com/auth0-samples/auth0-php-web-app/blob/master/LICENSE) file for more info. 25 | -------------------------------------------------------------------------------- /app/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /app/.env.example: -------------------------------------------------------------------------------- 1 | # --------- 2 | # Required: 3 | # --------- 4 | 5 | # Your Auth0 application's Client ID 6 | AUTH0_CLIENT_ID={CLIENT_ID} 7 | 8 | # The URL of your Auth0 tenant domain 9 | AUTH0_DOMAIN={DOMAIN} 10 | 11 | # Your Auth0 application's Client Secret 12 | AUTH0_CLIENT_SECRET={CLIENT_SECRET} 13 | 14 | # A long, secret value used to encrypt the session cookie 15 | AUTH0_COOKIE_SECRET={LONG_RANDOM_VALUE} 16 | 17 | # ------------------------------------------------------------------------- 18 | # Optional: Remove the leading # from the following options to enable them: 19 | # ------------------------------------------------------------------------- 20 | 21 | # How long the cookie session should last on the end-user device. (0 will clear when the browser window closes.) 22 | # AUTH0_COOKIE_EXPIRES=0 23 | 24 | # An API Identifier for testing custom APIs. 25 | # AUTH0_AUDIENCE={API_IDENTIFIER} 26 | 27 | # An Organization Id for testing Organizations. 28 | # AUTH0_ORGANIZATION={ORGANIZATION_ID} 29 | 30 | # -------------------------------------------------------------------------------------------------------------------------------- 31 | # Optional: Remove the leading # from ONE the following options to enable example behavior housed inside the /src/Example classes. 32 | # -------------------------------------------------------------------------------------------------------------------------------- 33 | 34 | # AUTH0_EXAMPLE=HttpEventListeners 35 | 36 | # Note that Passwordless requires using 'classic' Universal Login at this time. Read more: https://auth0.com/docs/universal-login/new-experience/new-experience-limitations 37 | # AUTH0_EXAMPLE=PasswordlessMagic 38 | -------------------------------------------------------------------------------- /app/.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 7 | ->setRules([ 8 | 'array_indentation' => true, 9 | 'array_push' => true, 10 | 'array_syntax' => ['syntax' => 'short'], 11 | 'assign_null_coalescing_to_coalesce_equal' => true, 12 | 'backtick_to_shell_exec' => true, 13 | 'binary_operator_spaces' => true, 14 | 'blank_line_after_namespace' => true, 15 | 'blank_line_after_opening_tag' => true, 16 | 'blank_line_before_statement' => true, 17 | 'blank_line_between_import_groups' => true, 18 | 'braces' => true, 19 | 'cast_spaces' => true, 20 | 'class_attributes_separation' => ['elements' => ['const' => 'one', 'method' => 'one', 'property' => 'one', 'trait_import' => 'one', 'case' => 'one']], 21 | '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], 22 | 'class_reference_name_casing' => true, 23 | 'clean_namespace' => true, 24 | 'combine_consecutive_issets' => true, 25 | 'combine_consecutive_unsets' => true, 26 | 'combine_nested_dirname' => true, 27 | 'comment_to_phpdoc' => ['ignored_tags' => ['codeCoverageIgnoreStart', 'codeCoverageIgnoreEnd', 'phpstan-ignore-next-line']], 28 | 'compact_nullable_typehint' => true, 29 | 'concat_space' => ['spacing' => 'one'], 30 | 'constant_case' => ['case' => 'lower'], 31 | '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], 32 | 'date_time_create_from_format_call' => true, 33 | 'date_time_immutable' => true, 34 | 'declare_equal_normalize' => ['space' => 'none'], 35 | 'declare_parentheses' => true, 36 | 'declare_strict_types' => true, 37 | 'dir_constant' => true, 38 | 'doctrine_annotation_array_assignment' => true, 39 | 'doctrine_annotation_braces' => true, 40 | 'doctrine_annotation_indentation' => true, 41 | 'doctrine_annotation_spaces' => true, 42 | 'echo_tag_syntax' => ['format' => 'long'], 43 | 'elseif' => true, 44 | 'empty_loop_body' => true, 45 | 'empty_loop_condition' => true, 46 | 'encoding' => true, 47 | 'ereg_to_preg' => true, 48 | 'error_suppression' => true, 49 | 'escape_implicit_backslashes' => true, 50 | 'explicit_indirect_variable' => true, 51 | 'explicit_string_variable' => true, 52 | 'final_class' => true, 53 | 'final_internal_class' => true, 54 | 'final_public_method_for_abstract_class' => true, 55 | 'fopen_flag_order' => true, 56 | 'fopen_flags' => true, 57 | 'full_opening_tag' => true, 58 | 'fully_qualified_strict_types' => true, 59 | 'function_declaration' => true, 60 | 'function_to_constant' => true, 61 | 'function_typehint_space' => true, 62 | 'general_phpdoc_annotation_remove' => true, 63 | 'general_phpdoc_tag_rename' => true, 64 | 'get_class_to_class_keyword' => true, 65 | 'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true], 66 | 'group_import' => true, 67 | 'heredoc_indentation' => true, 68 | 'heredoc_to_nowdoc' => true, 69 | 'implode_call' => true, 70 | 'include' => true, 71 | 'increment_style' => ['style' => 'pre'], 72 | 'indentation_type' => true, 73 | 'integer_literal_case' => true, 74 | 'is_null' => true, 75 | 'lambda_not_used_import' => true, 76 | 'line_ending' => true, 77 | 'linebreak_after_opening_tag' => true, 78 | 'list_syntax' => ['syntax' => 'short'], 79 | 'logical_operators' => true, 80 | 'lowercase_cast' => true, 81 | 'lowercase_keywords' => true, 82 | 'lowercase_static_reference' => true, 83 | 'magic_constant_casing' => true, 84 | 'magic_method_casing' => true, 85 | 'mb_str_functions' => false, 86 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline', 'after_heredoc' => true], 87 | 'method_chaining_indentation' => true, 88 | 'modernize_strpos' => true, 89 | 'modernize_types_casting' => true, 90 | 'multiline_comment_opening_closing' => true, 91 | 'multiline_whitespace_before_semicolons' => true, 92 | 'native_function_casing' => true, 93 | 'native_function_invocation' => true, 94 | 'native_function_type_declaration_casing' => true, 95 | 'new_with_braces' => true, 96 | 'no_alias_functions' => true, 97 | 'no_alias_language_construct_call' => true, 98 | 'no_alternative_syntax' => true, 99 | 'no_binary_string' => true, 100 | 'no_blank_lines_after_class_opening' => true, 101 | 'no_blank_lines_after_phpdoc' => true, 102 | 'no_break_comment' => true, 103 | 'no_closing_tag' => true, 104 | 'no_empty_comment' => true, 105 | 'no_empty_phpdoc' => true, 106 | 'no_empty_statement' => true, 107 | 'no_extra_blank_lines' => true, 108 | 'no_homoglyph_names' => true, 109 | 'no_leading_import_slash' => true, 110 | 'no_leading_namespace_whitespace' => true, 111 | 'no_mixed_echo_print' => true, 112 | 'no_multiline_whitespace_around_double_arrow' => true, 113 | 'no_multiple_statements_per_line' => true, 114 | 'no_php4_constructor' => true, 115 | 'no_short_bool_cast' => true, 116 | 'no_singleline_whitespace_before_semicolons' => true, 117 | 'no_space_around_double_colon' => true, 118 | 'no_spaces_after_function_name' => true, 119 | 'no_spaces_around_offset' => true, 120 | 'no_spaces_inside_parenthesis' => true, 121 | 'no_superfluous_elseif' => true, 122 | 'no_trailing_comma_in_singleline' => true, 123 | 'no_trailing_whitespace_in_comment' => true, 124 | 'no_trailing_whitespace_in_string' => true, 125 | 'no_trailing_whitespace' => true, 126 | 'no_unneeded_control_parentheses' => true, 127 | 'no_unneeded_curly_braces' => true, 128 | 'no_unneeded_final_method' => true, 129 | 'no_unneeded_import_alias' => true, 130 | 'no_unreachable_default_argument_value' => true, 131 | 'no_unset_cast' => true, 132 | 'no_unused_imports' => true, 133 | 'no_useless_concat_operator' => true, 134 | 'no_useless_else' => true, 135 | 'no_useless_nullsafe_operator' => true, 136 | 'no_useless_return' => true, 137 | 'no_useless_sprintf' => true, 138 | 'no_whitespace_before_comma_in_array' => true, 139 | 'no_whitespace_in_blank_line' => true, 140 | 'non_printable_character' => true, 141 | 'normalize_index_brace' => true, 142 | 'not_operator_with_successor_space' => true, 143 | 'nullable_type_declaration_for_default_null_value' => true, 144 | 'object_operator_without_whitespace' => true, 145 | 'octal_notation' => true, 146 | 'operator_linebreak' => true, 147 | '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']], 148 | 'ordered_imports' => ['sort_algorithm' => 'alpha', 'imports_order' => ['const', 'class', 'function']], 149 | 'ordered_interfaces' => true, 150 | 'ordered_traits' => true, 151 | 'php_unit_fqcn_annotation' => true, 152 | 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 153 | 'phpdoc_align' => ['align' => 'vertical'], 154 | 'phpdoc_indent' => true, 155 | 'phpdoc_inline_tag_normalizer' => true, 156 | 'phpdoc_line_span' => true, 157 | 'phpdoc_no_access' => true, 158 | 'phpdoc_no_empty_return' => true, 159 | 'phpdoc_no_package' => true, 160 | 'phpdoc_no_useless_inheritdoc' => true, 161 | 'phpdoc_order_by_value' => true, 162 | 'phpdoc_order' => true, 163 | 'phpdoc_return_self_reference' => ['replacements' => ['this' => 'self']], 164 | 'phpdoc_scalar' => true, 165 | 'phpdoc_separation' => true, 166 | 'phpdoc_single_line_var_spacing' => true, 167 | 'phpdoc_summary' => true, 168 | 'phpdoc_tag_type' => true, 169 | 'phpdoc_to_comment' => ['ignored_tags' => ['var']], 170 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 171 | 'phpdoc_trim' => true, 172 | 'phpdoc_types_order' => true, 173 | 'phpdoc_types' => true, 174 | 'phpdoc_var_annotation_correct_order' => true, 175 | 'phpdoc_var_without_name' => true, 176 | 'pow_to_exponentiation' => true, 177 | 'protected_to_private' => true, 178 | 'psr_autoloading' => true, 179 | 'random_api_migration' => true, 180 | 'regular_callable_call' => true, 181 | 'return_assignment' => true, 182 | 'return_type_declaration' => ['space_before' => 'none'], 183 | 'return_type_declaration' => true, 184 | 'self_accessor' => true, 185 | 'self_static_accessor' => true, 186 | 'semicolon_after_instruction' => true, 187 | 'set_type_to_cast' => true, 188 | 'short_scalar_cast' => true, 189 | 'simple_to_complex_string_variable' => true, 190 | 'simplified_if_return' => true, 191 | 'single_blank_line_at_eof' => true, 192 | 'single_blank_line_before_namespace' => true, 193 | 'single_class_element_per_statement' => true, 194 | 'single_line_after_imports' => true, 195 | 'single_line_comment_spacing' => true, 196 | 'single_line_comment_style' => ['comment_types' => ['hash']], 197 | 'single_line_throw' => true, 198 | 'single_quote' => true, 199 | 'single_space_after_construct' => true, 200 | 'single_space_around_construct' => true, 201 | 'single_trait_insert_per_statement' => true, 202 | 'space_after_semicolon' => true, 203 | 'standardize_increment' => true, 204 | 'standardize_not_equals' => true, 205 | 'statement_indentation' => true, 206 | 'static_lambda' => true, 207 | 'strict_comparison' => true, 208 | 'strict_param' => true, 209 | 'string_length_to_empty' => true, 210 | 'string_line_ending' => true, 211 | 'switch_case_semicolon_to_colon' => true, 212 | 'switch_case_space' => true, 213 | 'switch_continue_to_break' => true, 214 | 'ternary_operator_spaces' => true, 215 | 'ternary_to_elvis_operator' => true, 216 | 'ternary_to_null_coalescing' => true, 217 | 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arguments', 'arrays', 'match', 'parameters']], 218 | 'trim_array_spaces' => true, 219 | 'types_spaces' => ['space' => 'single', 'space_multiple_catch' => 'single'], 220 | 'unary_operator_spaces' => true, 221 | 'use_arrow_functions' => true, 222 | 'visibility_required' => true, 223 | 'void_return' => true, 224 | 'whitespace_after_comma_in_array' => true, 225 | 'yoda_style' => true, 226 | ]) 227 | ->setFinder( 228 | PhpCsFixer\Finder::create() 229 | ->exclude('vendor') 230 | ->in([__DIR__ . '/src/']), 231 | ); 232 | -------------------------------------------------------------------------------- /app/.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # Auth0 PHP Web App Sample 2 | 3 | This sample demonstrates how to add authorization to a [PHP](http://php.net/) web app using [Auth0](https://auth0.com). 4 | 5 | Check the [PHP Quickstart](https://auth0.com/docs/quickstart/webapp/php) to understand this sample better. 6 | 7 | ## Configuration 8 | 9 | ### Create a free account in Auth0 10 | 11 | 1. Go to [Auth0](https://auth0.com) and click Sign Up. 12 | 2. Use Google, GitHub or Microsoft Account to login. 13 | 14 | ### Create an Auth0 Application 15 | 16 | You will need to create a Regular Web Application using the [Auth0 Dashboard](https://manage.auth0.com). This will give you a Domain, Client ID, and Client Secret you will need below. 17 | 18 | ### Configure Credentials 19 | 20 | Your project needs to be configured with your Auth0 Domain, Client ID, and Client Secret for the authentication flow to work. 21 | 22 | Copy .env.example into a new file in the same folder called .env, and replace the values with your Auth0 application credentials: 23 | 24 | ```sh 25 | # Your Auth0 application's Client ID 26 | AUTH0_CLIENT_ID='YOUR_AUTH0_CLIENT_ID' 27 | 28 | # The url of your Auth0 tenant domain 29 | AUTH0_DOMAIN='https://YOUR_AUTH0_DOMAIN.auth0.com' 30 | 31 | # Your Auth0 application's Client Secret 32 | AUTH0_CLIENT_SECRET='YOUR_AUTH0_CLIENT_SECRET' 33 | 34 | # A long secret value used to encrypt the session cookie 35 | AUTH0_COOKIE_SECRET='LONG_RANDOM_VALUE' 36 | ``` 37 | 38 | **Note**: Make sure you replace `LONG_RANDOM_VALUE` with your secret (you can generate a suitable string using `openssl rand -hex 32` on the command line). 39 | 40 | **Note**: Ensure you are consistent in your use of 'localhost' and/or '127.0.0.1' when testing. These must match your Auth0 Application settings or you will encounter errors. They must also match for session cookies to work correctly. 41 | 42 | ## Install dependencies 43 | 44 | Please ensure you have [Composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos) installed and accessible from your shell. This is required. 45 | 46 | ```bash 47 | composer install --no-dev 48 | ``` 49 | 50 | ## Run the sample 51 | 52 | Before continuing, please ensure you have [PHP](https://www.php.net/manual/en/install.php) 7.4+ and [Composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos) installed and accessible from your shell. These are required. 53 | 54 | Next, use the following command to install the necessary dependencies and start the sample: 55 | 56 | ```bash 57 | php -S 127.0.0.1:3000 public/index.php 58 | ``` 59 | 60 | Your Quickstart should now be accessible at [http://127.0.0.1:3000/](http://127.0.0.1:3000/) from your web browser. 61 | 62 | ## Vulnerability Reporting 63 | 64 | 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. 65 | 66 | ## What is Auth0? 67 | 68 | Auth0 helps you to easily: 69 | 70 | - implement authentication with multiple identity providers, including social (e.g., Google, Facebook, Microsoft, LinkedIn, GitHub, Twitter, etc), or enterprise (e.g., Windows Azure AD, Google Apps, Active Directory, ADFS, SAML, etc.) 71 | - log in users with username/password databases, passwordless, or multi-factor authentication 72 | - link multiple user accounts together 73 | - generate signed JSON Web Tokens to authorize your API calls and flow the user identity securely 74 | - access demographics and analytics detailing how, when, and where users are logging in 75 | - enrich user profiles from other data sources using customizable JavaScript rules 76 | 77 | [Why Auth0?](https://auth0.com/why-auth0) 78 | 79 | ## License 80 | 81 | This project is licensed under the MIT license. See the [LICENSE](https://github.com/auth0-samples/auth0-php-web-app/blob/master/LICENSE) file for more info. 82 | -------------------------------------------------------------------------------- /app/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0-samples/auth0-php-web-app", 3 | "description": "Auth0 Integration Samples for PHP Web Applications.", 4 | "require": { 5 | "php": "^8.0", 6 | "auth0/auth0-php": "^8.0", 7 | "guzzlehttp/guzzle": "^7.3", 8 | "guzzlehttp/psr7": "^2.2", 9 | "http-interop/http-factory-guzzle": "^1.0", 10 | "hyperf/event": "^2.1", 11 | "php-http/httplug": "^2.2", 12 | "vlucas/phpdotenv": "^5.3" 13 | }, 14 | "require-dev": { 15 | "friendsofphp/php-cs-fixer": "^3", 16 | "mockery/mockery": "^1", 17 | "nunomaduro/larastan": "^2", 18 | "orchestra/testbench": "^7 || ^8", 19 | "pestphp/pest": "^2", 20 | "pestphp/pest-plugin-laravel": "^2", 21 | "phpstan/phpstan": "^1", 22 | "phpstan/phpstan-strict-rules": "^1", 23 | "psalm/plugin-laravel": "^2", 24 | "rector/rector": "0.17.0", 25 | "spatie/laravel-ray": "^1", 26 | "squizlabs/php_codesniffer": "^3", 27 | "symfony/cache": "^6", 28 | "vimeo/psalm": "^5", 29 | "wikimedia/composer-merge-plugin": "^2" 30 | }, 31 | "config": { 32 | "optimize-autoloader": true, 33 | "sort-packages": true, 34 | "allow-plugins": { 35 | "dealerdirect/phpcodesniffer-composer-installer": true, 36 | "friendsofphp/well-known-implementations": false, 37 | "wikimedia/composer-merge-plugin": true, 38 | "php-http/discovery": false, 39 | "pestphp/pest-plugin": true 40 | } 41 | }, 42 | "scripts": { 43 | "pest:coverage": "@php vendor/bin/pest --coverage --parallel --no-progress", 44 | "pest:debug": "@php vendor/bin/pest --fail-on-risky --stop-on-defect", 45 | "pest:profile": "@php vendor/bin/pest --profile", 46 | "pest": "@php vendor/bin/pest --order-by random --fail-on-risky --stop-on-defect --coverage --parallel", 47 | "phpcs:fix": "@php vendor/bin/php-cs-fixer fix", 48 | "phpcs": "@php vendor/bin/php-cs-fixer fix --dry-run --diff", 49 | "phpstan": "@php vendor/bin/phpstan analyze", 50 | "psalm:fix": "@php vendor/bin/psalter --issues=all", 51 | "psalm": "@php vendor/bin/psalm", 52 | "rector:fix": "@php vendor/bin/rector process src", 53 | "rector": "@php vendor/bin/rector process src --dry-run", 54 | "test": [ 55 | "Composer\\Config::disableProcessTimeout", 56 | "@pest", 57 | "@phpstan", 58 | "@psalm", 59 | "@rector", 60 | "@phpcs" 61 | ] 62 | }, 63 | "license": "MIT" 64 | } 65 | -------------------------------------------------------------------------------- /app/phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/phpstan/phpstan-strict-rules/rules.neon 3 | - ./vendor/nunomaduro/larastan/extension.neon 4 | 5 | parameters: 6 | level: max 7 | 8 | paths: 9 | - src 10 | 11 | ignoreErrors: 12 | - '#Method (.*) has parameter (.*) with no value type specified in iterable type array.#' 13 | - '#no value type specified in iterable type array.#' 14 | 15 | reportUnmatchedIgnoredErrors: false 16 | treatPhpDocTypesAsCertain: false 17 | checkGenericClassInNonGenericObjectType: false 18 | -------------------------------------------------------------------------------- /app/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/public/bootstrap.php: -------------------------------------------------------------------------------- 1 | load(); 30 | 31 | // Instantiate our Quickstart Application using the .env configuration. 32 | $app = new Application($_ENV); 33 | 34 | if (isset($_ENV['AUTH0_EXAMPLE'])) { 35 | require_once join(DIRECTORY_SEPARATOR, [APP_ROOT, 'src/Contract/QuickstartExample.php']); 36 | require_once join(DIRECTORY_SEPARATOR, [APP_ROOT, 'src/Example', $_ENV['AUTH0_EXAMPLE'] . '.php']); 37 | 38 | $className = '\Auth0\Quickstart\Example\\' . $_ENV['AUTH0_EXAMPLE']; 39 | 40 | $app->useExample(new $className($app)); 41 | } 42 | 43 | $app->run(); 44 | -------------------------------------------------------------------------------- /app/rector.php: -------------------------------------------------------------------------------- 1 | paths([ 209 | __DIR__ . '/config', 210 | __DIR__ . '/src', 211 | ]); 212 | 213 | $rectorConfig->ruleWithConfiguration( 214 | RenameFunctionRector::class, 215 | [ 216 | 'chop' => 'rtrim', 217 | 'doubleval' => 'floatval', 218 | 'fputs' => 'fwrite', 219 | 'gzputs' => 'gzwrites', 220 | 'ini_alter' => 'ini_set', 221 | 'is_double' => 'is_float', 222 | 'is_integer' => 'is_int', 223 | 'is_long' => 'is_int', 224 | 'is_real' => 'is_float', 225 | 'is_writeable' => 'is_writable', 226 | 'join' => 'implode', 227 | 'key_exists' => 'array_key_exists', 228 | 'mbstrcut' => 'mb_strcut', 229 | 'mbstrlen' => 'mb_strlen', 230 | 'mbstrpos' => 'mb_strpos', 231 | 'mbstrrpos' => 'mb_strrpos', 232 | 'mbsubstr' => 'mb_substr', 233 | 'pos' => 'current', 234 | 'sizeof' => 'count', 235 | 'split' => 'explode', 236 | 'strchr' => 'strstr', 237 | ], 238 | ); 239 | 240 | $rectorConfig->ruleWithConfiguration( 241 | StaticCallToFuncCallRector::class, 242 | [ 243 | new StaticCallToFuncCall('Nette\\Utils\\Strings', 'contains', 'str_contains'), 244 | new StaticCallToFuncCall('Nette\\Utils\\Strings', 'endsWith', 'str_ends_with'), 245 | new StaticCallToFuncCall('Nette\\Utils\\Strings', 'startsWith', 'str_starts_with'), 246 | ], 247 | ); 248 | 249 | $rectorConfig->ruleWithConfiguration( 250 | ArgumentAdderRector::class, 251 | [new ArgumentAdder('Nette\\Utils\\Strings', 'replace', 2, 'replacement', '')], 252 | ); 253 | 254 | $rectorConfig->ruleWithConfiguration( 255 | RenameFunctionRector::class, 256 | [ 257 | 'pg_clientencoding' => 'pg_client_encoding', 258 | 'pg_cmdtuples' => 'pg_affected_rows', 259 | 'pg_errormessage' => 'pg_last_error', 260 | 'pg_fieldisnull' => 'pg_field_is_null', 261 | 'pg_fieldname' => 'pg_field_name', 262 | 'pg_fieldnum' => 'pg_field_num', 263 | 'pg_fieldprtlen' => 'pg_field_prtlen', 264 | 'pg_fieldsize' => 'pg_field_size', 265 | 'pg_fieldtype' => 'pg_field_type', 266 | 'pg_freeresult' => 'pg_free_result', 267 | 'pg_getlastoid' => 'pg_last_oid', 268 | 'pg_loclose' => 'pg_lo_close', 269 | 'pg_locreate' => 'pg_lo_create', 270 | 'pg_loexport' => 'pg_lo_export', 271 | 'pg_loimport' => 'pg_lo_import', 272 | 'pg_loopen' => 'pg_lo_open', 273 | 'pg_loread' => 'pg_lo_read', 274 | 'pg_loreadall' => 'pg_lo_read_all', 275 | 'pg_lounlink' => 'pg_lo_unlink', 276 | 'pg_lowrite' => 'pg_lo_write', 277 | 'pg_numfields' => 'pg_num_fields', 278 | 'pg_numrows' => 'pg_num_rows', 279 | 'pg_result' => 'pg_fetch_result', 280 | 'pg_setclientencoding' => 'pg_set_client_encoding', 281 | ], 282 | ); 283 | 284 | $rectorConfig->ruleWithConfiguration( 285 | FunctionArgumentDefaultValueReplacerRector::class, 286 | [ 287 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'), 288 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'), 289 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '', '!='), 290 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '!', '!='), 291 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'g', 'gt'), 292 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'l', 'lt'), 293 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'), 294 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'), 295 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'n', 'ne'), 296 | ], 297 | ); 298 | 299 | $rectorConfig->ruleWithConfiguration( 300 | FuncCallToConstFetchRector::class, 301 | [ 302 | 'php_sapi_name' => 'PHP_SAPI', 303 | 'pi' => 'M_PI', 304 | ], 305 | ); 306 | 307 | $rectorConfig->rules([ 308 | AbsolutizeRequireAndIncludePathRector::class, 309 | ActionInjectionToConstructorInjectionRector::class, 310 | AddArrayDefaultToArrayPropertyRector::class, 311 | AddArrowFunctionReturnTypeRector::class, 312 | AddClosureReturnTypeRector::class, 313 | AddMethodCallBasedStrictParamTypeRector::class, 314 | AddParamBasedOnParentClassMethodRector::class, 315 | AddParamTypeBasedOnPHPUnitDataProviderRector::class, 316 | AddParamTypeSplFixedArrayRector::class, 317 | AddReturnTypeDeclarationBasedOnParentClassMethodRector::class, 318 | AddReturnTypeDeclarationFromYieldsRector::class, 319 | AddVoidReturnTypeWhereNoReturnRector::class, 320 | AndAssignsToSeparateLinesRector::class, 321 | ArrayKeyExistsTernaryThenValueToCoalescingRector::class, 322 | ArrayKeysAndInArrayToArrayKeyExistsRector::class, 323 | ArrayMergeOfNonArraysToSimpleArrayRector::class, 324 | ArrayShapeFromConstantArrayReturnRector::class, 325 | BinarySwitchToIfElseRector::class, 326 | BooleanNotIdenticalToNotIdenticalRector::class, 327 | BoolvalToTypeCastRector::class, 328 | CallableThisArrayToAnonymousFunctionRector::class, 329 | CallUserFuncArrayToVariadicRector::class, 330 | CallUserFuncToMethodCallRector::class, 331 | CallUserFuncToMethodCallRector::class, 332 | CallUserFuncWithArrowFunctionToInlineRector::class, 333 | CatchExceptionNameMatchingTypeRector::class, 334 | ChangeArrayPushToArrayAssignRector::class, 335 | ChangeGlobalVariablesToPropertiesRector::class, 336 | ChangeIfElseValueAssignToEarlyReturnRector::class, 337 | ChangeNestedForeachIfsToEarlyContinueRector::class, 338 | ChangeNestedIfsToEarlyReturnRector::class, 339 | ChangeOrIfContinueToMultiContinueRector::class, 340 | ChangeSwitchToMatchRector::class, 341 | ClassOnObjectRector::class, 342 | ClassOnThisVariableObjectRector::class, 343 | ClassPropertyAssignToConstructorPromotionRector::class, 344 | CombinedAssignRector::class, 345 | CombineIfRector::class, 346 | CommonNotEqualRector::class, 347 | CompactToVariablesRector::class, 348 | CompleteDynamicPropertiesRector::class, 349 | ConsecutiveNullCompareReturnsToNullCoalesceQueueRector::class, 350 | ConsistentImplodeRector::class, 351 | CountArrayToEmptyArrayComparisonRector::class, 352 | CountArrayToEmptyArrayComparisonRector::class, 353 | EmptyOnNullableObjectToInstanceOfRector::class, 354 | EncapsedStringsToSprintfRector::class, 355 | ExplicitBoolCompareRector::class, 356 | FinalizeClassesWithoutChildrenRector::class, 357 | FinalPrivateToPrivateVisibilityRector::class, 358 | FlipTypeControlToUseExclusiveTypeRector::class, 359 | FloatvalToTypeCastRector::class, 360 | ForeachItemsAssignToEmptyArrayToAssignRector::class, 361 | ForeachToInArrayRector::class, 362 | ForRepeatedCountToOwnVariableRector::class, 363 | FuncGetArgsToVariadicParamRector::class, 364 | FuncGetArgsToVariadicParamRector::class, 365 | GetClassToInstanceOfRector::class, 366 | GetDebugTypeRector::class, 367 | InlineArrayReturnAssignRector::class, 368 | InlineConstructorDefaultToPropertyRector::class, 369 | InlineIfToExplicitIfRector::class, 370 | InlineIsAInstanceOfRector::class, 371 | IntvalToTypeCastRector::class, 372 | IsAWithStringWithThirdArgumentRector::class, 373 | IssetOnPropertyObjectToPropertyExistsRector::class, 374 | JoinStringConcatRector::class, 375 | LogicalToBooleanRector::class, 376 | MakeInheritedMethodVisibilitySameAsParentRector::class, 377 | MultipleClassFileToPsr4ClassesRector::class, 378 | NewlineBeforeNewAssignSetRector::class, 379 | NewStaticToNewSelfRector::class, 380 | NormalizeNamespaceByPSR4ComposerAutoloadRector::class, 381 | NullableCompareToNullRector::class, 382 | OptionalParametersAfterRequiredRector::class, 383 | OptionalParametersAfterRequiredRector::class, 384 | ParamTypeByMethodCallTypeRector::class, 385 | ParamTypeByParentCallTypeRector::class, 386 | ParamTypeFromStrictTypedPropertyRector::class, 387 | Php8ResourceReturnToObjectRector::class, 388 | PostIncDecToPreIncDecRector::class, 389 | PreparedValueToEarlyReturnRector::class, 390 | PrivatizeFinalClassMethodRector::class, 391 | PrivatizeFinalClassPropertyRector::class, 392 | PropertyTypeFromStrictSetterGetterRector::class, 393 | RemoveAlwaysElseRector::class, 394 | RemoveAlwaysTrueConditionSetInConstructorRector::class, 395 | RemoveAndTrueRector::class, 396 | RemoveDeadConditionAboveReturnRector::class, 397 | RemoveDeadContinueRector::class, 398 | RemoveDeadIfForeachForRector::class, 399 | RemoveDeadLoopRector::class, 400 | RemoveDeadReturnRector::class, 401 | RemoveDeadStmtRector::class, 402 | RemoveDeadTryCatchRector::class, 403 | RemoveDeadZeroAndOneOperationRector::class, 404 | RemoveDelegatingParentCallRector::class, 405 | RemoveDoubleAssignRector::class, 406 | RemoveDuplicatedArrayKeyRector::class, 407 | RemoveDuplicatedCaseInSwitchRector::class, 408 | RemoveEmptyClassMethodRector::class, 409 | RemoveEmptyTestMethodRector::class, 410 | RemoveExtraParametersRector::class, 411 | RemoveFinalFromConstRector::class, 412 | RemoveJustPropertyFetchForAssignRector::class, 413 | RemoveJustVariableAssignRector::class, 414 | RemoveLastReturnRector::class, 415 | RemoveNonExistingVarAnnotationRector::class, 416 | RemoveNullPropertyInitializationRector::class, 417 | RemoveParentCallWithoutParentRector::class, 418 | RemoveParentCallWithoutParentRector::class, 419 | RemoveSoleValueSprintfRector::class, 420 | RemoveUnreachableStatementRector::class, 421 | RemoveUnusedConstructorParamRector::class, 422 | RemoveUnusedForeachKeyRector::class, 423 | RemoveUnusedNonEmptyArrayBeforeForeachRector::class, 424 | RemoveUnusedPrivateClassConstantRector::class, 425 | RemoveUnusedPrivateMethodParameterRector::class, 426 | RemoveUnusedPrivatePropertyRector::class, 427 | RemoveUnusedPromotedPropertyRector::class, 428 | RemoveUnusedVariableAssignRector::class, 429 | RemoveUnusedVariableInCatchRector::class, 430 | RemoveUselessReturnTagRector::class, 431 | RemoveUselessVarTagRector::class, 432 | RenameForeachValueVariableToMatchExprVariableRector::class, 433 | RenameForeachValueVariableToMatchMethodCallReturnTypeRector::class, 434 | ReplaceMultipleBooleanNotRector::class, 435 | ReturnAnnotationIncorrectNullableRector::class, 436 | ReturnBinaryAndToEarlyReturnRector::class, 437 | ReturnBinaryOrToEarlyReturnRector::class, 438 | ReturnEarlyIfVariableRector::class, 439 | ReturnNeverTypeRector::class, 440 | ReturnTypeFromReturnDirectArrayRector::class, 441 | ReturnTypeFromReturnNewRector::class, 442 | ReturnTypeFromStrictBoolReturnExprRector::class, 443 | ReturnTypeFromStrictConstantReturnRector::class, 444 | ReturnTypeFromStrictNativeCallRector::class, 445 | ReturnTypeFromStrictNewArrayRector::class, 446 | ReturnTypeFromStrictScalarReturnExprRector::class, 447 | ReturnTypeFromStrictScalarReturnExprRector::class, 448 | ReturnTypeFromStrictTernaryRector::class, 449 | ReturnTypeFromStrictTypedCallRector::class, 450 | ReturnTypeFromStrictTypedPropertyRector::class, 451 | SeparateMultiUseImportsRector::class, 452 | SetStateToStaticRector::class, 453 | SetTypeToCastRector::class, 454 | ShortenElseIfRector::class, 455 | SimplifyArraySearchRector::class, 456 | SimplifyBoolIdenticalTrueRector::class, 457 | SimplifyConditionsRector::class, 458 | SimplifyDeMorganBinaryRector::class, 459 | SimplifyEmptyArrayCheckRector::class, 460 | SimplifyEmptyCheckOnEmptyArrayRector::class, 461 | SimplifyForeachToArrayFilterRector::class, 462 | SimplifyForeachToCoalescingRector::class, 463 | SimplifyFuncGetArgsCountRector::class, 464 | SimplifyIfElseToTernaryRector::class, 465 | SimplifyIfElseWithSameContentRector::class, 466 | SimplifyIfNotNullReturnRector::class, 467 | SimplifyIfNullableReturnRector::class, 468 | SimplifyIfReturnBoolRector::class, 469 | SimplifyInArrayValuesRector::class, 470 | SimplifyMirrorAssignRector::class, 471 | SimplifyRegexPatternRector::class, 472 | SimplifyStrposLowerRector::class, 473 | SimplifyTautologyTernaryRector::class, 474 | SimplifyUselessVariableRector::class, 475 | SingleInArrayToCompareRector::class, 476 | SingularSwitchToIfRector::class, 477 | SplitDoubleAssignRector::class, 478 | SplitGroupedClassConstantsRector::class, 479 | SplitGroupedPropertiesRector::class, 480 | StaticArrowFunctionRector::class, 481 | StaticClosureRector::class, 482 | StrContainsRector::class, 483 | StrEndsWithRector::class, 484 | StrictArraySearchRector::class, 485 | StringableForToStringRector::class, 486 | StrlenZeroToIdenticalEmptyStringRector::class, 487 | StrStartsWithRector::class, 488 | StrvalToTypeCastRector::class, 489 | SwitchNegatedTernaryRector::class, 490 | SymplifyQuoteEscapeRector::class, 491 | TernaryConditionVariableAssignmentRector::class, 492 | TernaryEmptyArrayArrayDimFetchToCoalesceRector::class, 493 | TernaryFalseExpressionToIfRector::class, 494 | TernaryToBooleanOrFalseToBooleanAndRector::class, 495 | ThrowWithPreviousExceptionRector::class, 496 | TypedPropertyFromAssignsRector::class, 497 | TypedPropertyFromStrictConstructorRector::class, 498 | TypedPropertyFromStrictGetterMethodReturnTypeRector::class, 499 | TypedPropertyFromStrictSetUpRector::class, 500 | UnnecessaryTernaryExpressionRector::class, 501 | UnSpreadOperatorRector::class, 502 | UnusedForeachValueToArrayKeysRector::class, 503 | UnwrapFutureCompatibleIfPhpVersionRector::class, 504 | UnwrapSprintfOneArgumentRector::class, 505 | UseClassKeywordForClassNameResolutionRector::class, 506 | UseIdenticalOverEqualWithSameTypeRector::class, 507 | UseIncrementAssignRector::class, 508 | VarAnnotationIncorrectNullableRector::class, 509 | VarConstantCommentRector::class, 510 | VarToPublicPropertyRector::class, 511 | VersionCompareFuncCallToConstantRector::class, 512 | WrapEncapsedVariableInCurlyBracesRector::class, 513 | IfIssetToCoalescingRector::class, 514 | CleanupUnneededNullsafeOperatorRector::class, 515 | BoolReturnTypeFromStrictScalarReturnsRector::class, 516 | ]); 517 | }; 518 | -------------------------------------------------------------------------------- /app/src/Application.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | private array $exampleHooks = []; 34 | 35 | /** 36 | * An instance of our application's router class, for handling end-user requests to URIs. 37 | */ 38 | private ApplicationRouter $router; 39 | 40 | /** 41 | * An instance of the Auth0 SDK. 42 | */ 43 | private Auth0 $sdk; 44 | 45 | /** 46 | * An instance of our application's template rendering helper class, for sending responses. 47 | */ 48 | private ApplicationTemplates $templates; 49 | 50 | /** 51 | * Setup our Quickstart application. 52 | * 53 | * @param array $env Auth0 configuration imported from .env file. 54 | */ 55 | public function __construct( 56 | array $env, 57 | ) { 58 | // Configure the SDK using our .env configuration. 59 | $this->setupAuth0($env); 60 | 61 | // Setup our template engine, for sending responses back to the browser. 62 | $this->templates = new ApplicationTemplates($this); 63 | $this->errorHandler = new ApplicationErrorHandler($this); 64 | $this->router = new ApplicationRouter($this); 65 | } 66 | 67 | /** 68 | * Return our instance of Auth0. 69 | */ 70 | public function &getSdk(): Auth0 71 | { 72 | return $this->sdk; 73 | } 74 | 75 | /** 76 | * Return our instance of SdkConfiguration. 77 | */ 78 | public function &getConfiguration(): SdkConfiguration 79 | { 80 | return $this->configuration; 81 | } 82 | 83 | /** 84 | * Return our instance of ApplicationTemplates. 85 | */ 86 | public function &getTemplate(): ApplicationTemplates 87 | { 88 | return $this->templates; 89 | } 90 | 91 | /** 92 | * Return our instance of ApplicationErrorHandler. 93 | */ 94 | public function &getErrorHandler(): ApplicationErrorHandler 95 | { 96 | return $this->errorHandler; 97 | } 98 | 99 | /** 100 | * Return our instance of ApplicationRouter. 101 | */ 102 | public function &getRouter(): ApplicationRouter 103 | { 104 | return $this->router; 105 | } 106 | 107 | /** 108 | * "Register" a QuickstartExample class. 109 | * 110 | * @param string $eventName 111 | * @param callable $callback 112 | */ 113 | public function hook( 114 | string $eventName, 115 | callable $callback, 116 | ): self { 117 | $this->exampleHooks[$eventName] = $callback; 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Called from the ApplicationRouter when end user loads '/callback'. 124 | * 125 | * @param ApplicationRouter $router 126 | */ 127 | public function onCallbackRoute( 128 | ApplicationRouter $router, 129 | ): void { 130 | // If you have an example class enabled ("AUTH0_EXAMPLE" in your .env file), check if a hook is setup to override default behavior: 131 | $event = $this->exampleHooks['onCallbackRoute'] ?? null; 132 | 133 | if (null === $event || null === $event($router)) { 134 | // Inform Auth0 we want to redirect to our /callback route, so we can perform the code exchange and setup the user session there. 135 | $this->sdk->exchange($router->getUri('/callback', '')); 136 | 137 | // Redirect to your application's index route. 138 | $router->redirect($router->getUri('/', '')); 139 | } 140 | } 141 | 142 | /** 143 | * Called from the ApplicationRouter when end user loads an unknown route. 144 | * 145 | * @param ApplicationRouter $router 146 | */ 147 | public function onError404( 148 | ApplicationRouter $router, 149 | ): void { 150 | $router->setHttpStatus(404); 151 | } 152 | 153 | /** 154 | * Called from the ApplicationRouter when end user loads '/'. 155 | * 156 | * @param ApplicationRouter $router 157 | */ 158 | public function onIndexRoute( 159 | ApplicationRouter $router, 160 | ): void { 161 | // Retrieve current session credentials, if end user is signed in. 162 | $session = $this->sdk->getCredentials(); 163 | 164 | // If a session is available, check if the token is expired. 165 | // @phpstan-ignore-next-line 166 | if (null !== $session && $session->accessTokenExpired) { 167 | try { 168 | // Token has expired, attempt to renew it. 169 | $this->sdk->renew(); 170 | } catch (\Auth0\SDK\Exception\StateException) { 171 | // There was an error during access token renewal. Clear the session. 172 | $this->sdk->clear(); 173 | $session = null; 174 | } 175 | } 176 | 177 | // If you have an example class enabled ("AUTH0_EXAMPLE" in your .env file), check if a hook is setup to override default behavior: 178 | $event = $this->exampleHooks['onIndexRoute'] ?? null; 179 | 180 | if (null === $event || null === $event($router, $session)) { 181 | // Send response to browser. 182 | $this->templates->render('logged-' . (null === $session ? 'out' : 'in'), [ 183 | 'session' => $session, 184 | 'router' => $router, 185 | 'cookies' => $_COOKIE, 186 | ]); 187 | } 188 | } 189 | 190 | /** 191 | * Called from the ApplicationRouter when end user loads '/login'. 192 | * 193 | * @param ApplicationRouter $router 194 | */ 195 | public function onLoginRoute( 196 | ApplicationRouter $router, 197 | ): void { 198 | // Clear the local session. 199 | $this->sdk->clear(); 200 | 201 | // If you have an example class enabled ("AUTH0_EXAMPLE" in your .env file), check if a hook is setup to override default behavior: 202 | $event = $this->exampleHooks['onLoginRoute'] ?? null; 203 | 204 | if (null === $event || null === $event($router)) { 205 | // Redirect to Auth0's Universal Login page. 206 | $router->redirect($this->sdk->login($router->getUri('/callback', ''))); 207 | } 208 | } 209 | 210 | /** 211 | * Called from the ApplicationRouter when end user loads '/logout'. 212 | * 213 | * @param ApplicationRouter $router 214 | */ 215 | public function onLogoutRoute( 216 | ApplicationRouter $router, 217 | ): void { 218 | // Redirect to Auth0's Universal Login page. 219 | $router->redirect($this->sdk->logout($router->getUri('/', ''))); 220 | } 221 | 222 | /** 223 | * "Run" our application, responding to end-user requests. 224 | */ 225 | public function run(): void 226 | { 227 | // Intercept exceptions to gracefully report them. 228 | $this->errorHandler->hook(); 229 | 230 | // Handle incoming requests through the router. 231 | $this->router->run(); 232 | } 233 | 234 | /** 235 | * Configure the Auth0 SDK using the .env configuration. 236 | * 237 | * @param array $env Auth0 configuration imported from .env file. 238 | */ 239 | public function setupAuth0( 240 | array $env, 241 | ): void { 242 | // Build our SdkConfiguration. 243 | $this->configuration = new SdkConfiguration([ 244 | 'domain' => $env['AUTH0_DOMAIN'] ?? null, 245 | 'customDomain' => $env['AUTH0_CUSTOM_DOMAIN'] ?? null, 246 | 'clientId' => $env['AUTH0_CLIENT_ID'] ?? null, 247 | 'clientSecret' => $env['AUTH0_CLIENT_SECRET'] ?? null, 248 | 'cookieSecret' => $env['AUTH0_COOKIE_SECRET'] ?? null, 249 | 'cookieExpires' => (int) ($env['AUTH0_COOKIE_EXPIRES'] ?? 60 * 60 * 24), 250 | 'audience' => ($env['AUTH0_AUDIENCE'] ?? null) !== null ? [trim($env['AUTH0_AUDIENCE'])] : null, 251 | 'organization' => ($env['AUTH0_ORGANIZATION'] ?? null) !== null ? [trim($env['AUTH0_ORGANIZATION'])] : null, 252 | ]); 253 | 254 | // Add 'offline_access' to scopes to ensure we get a renew token. 255 | $this->configuration->pushScope('offline_access'); 256 | 257 | // Setup the Auth0 SDK. 258 | $this->sdk = new Auth0($this->configuration); 259 | } 260 | 261 | /** 262 | * "Register" a QuickstartExample class. 263 | * 264 | * @param QuickstartExample $class 265 | */ 266 | public function useExample( 267 | QuickstartExample $class, 268 | ): self { 269 | $this->example = &$class; 270 | $this->example->setup(); 271 | 272 | return $this; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /app/src/ApplicationErrorHandler.php: -------------------------------------------------------------------------------- 1 | app = &$app; 28 | } 29 | 30 | /** 31 | * Setup onException() as the exception handler with PHP for this request. 32 | */ 33 | public function hook(): void 34 | { 35 | set_exception_handler(function (Throwable $throwable): void { 36 | $this->onException($throwable); 37 | }); 38 | } 39 | 40 | /** 41 | * Render a throwable error in a graceful way. 42 | * 43 | * @param Throwable $throwable The throwable to report. 44 | */ 45 | public function onException( 46 | Throwable $throwable, 47 | ): void { 48 | $exception = $throwable; 49 | 50 | $code = $exception->getCode(); 51 | $error = $exception->getMessage(); 52 | $file = $exception->getFile(); 53 | $line = $exception->getLine(); 54 | 55 | if ($exception instanceof Auth0Exception) { 56 | $backtrace = $exception->getTrace(); 57 | 58 | if (array_key_exists(1, $backtrace)) { 59 | $code = $backtrace[1]['line']; 60 | $file = $backtrace[1]['file']; 61 | $line = $backtrace[1]['line']; 62 | } 63 | } 64 | 65 | $this->app->getTemplate()->render('error', [ 66 | 'code' => $code, 67 | 'error' => $error, 68 | 'file' => $file, 69 | 'line' => $line, 70 | 'cookies' => $_COOKIE, 71 | 'router' => $this->app->getRouter(), 72 | ]); 73 | 74 | exit; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/ApplicationRouter.php: -------------------------------------------------------------------------------- 1 | app = &$app; 25 | } 26 | 27 | /** 28 | * Return the request method (GET, POST, etc.). 29 | */ 30 | public function getMethod(): string 31 | { 32 | return $_SERVER['REQUEST_METHOD'] ?? 'GET'; 33 | } 34 | 35 | /** 36 | * Return (and optionally manipulate) the currently requested uri. 37 | * 38 | * @param null|string $path Unless null, manipulates the resulting path to match the value. 39 | * @param null|string $query Unless, manipulates the resulting query to match the value. 40 | */ 41 | public function getUri( 42 | ?string $path = null, 43 | ?string $query = null, 44 | ): string { 45 | $httpScheme = $_SERVER['HTTPS'] ?? ''; 46 | $httpScheme = 'on' === $httpScheme ? 'https' : 'http'; 47 | 48 | $httpPort = (int) $_SERVER['SERVER_PORT']; 49 | $httpHost = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']; 50 | $httpHost = preg_replace('/\:' . $httpPort . '$/', '', $httpHost); 51 | 52 | $httpRequest = (string) $_SERVER['REQUEST_URI']; 53 | $httpUri = $httpScheme . '://' . $httpHost . (80 !== $httpPort ? ':' . $httpPort : '') . $httpRequest; 54 | 55 | // If we aren't making changes, simply return the uri. 56 | if (null === $path && null === $query) { 57 | return $httpUri; 58 | } 59 | 60 | // Parse a url into it's components so we can manipulate them more easily. 61 | $parsedUri = parse_url($httpUri); 62 | 63 | if (false === $parsedUri) { 64 | return $httpUri; 65 | } 66 | 67 | $parsedUri['scheme'] ??= 'http'; 68 | $parsedUri['host'] ??= $httpHost; 69 | $parsedUri['path'] ??= ''; 70 | $parsedUri['query'] = '?' . ($parsedUri['query'] ?? ''); 71 | 72 | // Manipulate the /path portion of the uri. 73 | if (null !== $path) { 74 | $parsedUri['path'] = $path; 75 | } 76 | 77 | // Manipulate the ?query portion of the uri. 78 | if (null !== $query) { 79 | $parsedUri['query'] = $query; 80 | } 81 | 82 | if (! array_key_exists('port', $parsedUri)) { 83 | $parsedUri['port'] = 80; 84 | } 85 | 86 | if ('?' === $parsedUri['query']) { 87 | $parsedUri['query'] = ''; 88 | } 89 | 90 | if ('' !== $parsedUri['query']) { 91 | $parsedUri['query'] = '?' . $parsedUri['query']; 92 | } 93 | 94 | // Reconstruct the manipulated uri and return it. 95 | return $parsedUri['scheme'] . '://' . $parsedUri['host'] . (80 !== $parsedUri['port'] ? ':' . $parsedUri['port'] : '') . $parsedUri['path'] . $parsedUri['query']; 96 | } 97 | 98 | /** 99 | * Process the current request and route it to the class handler. 100 | * 101 | * @param string $uri The new uri to redirect the end user to. 102 | */ 103 | public function redirect( 104 | string $uri, 105 | ): void { 106 | header('Location: ' . $uri, true, 303); 107 | exit; 108 | } 109 | 110 | /** 111 | * Process the current request and route it to the class handler. 112 | */ 113 | public function run(): void 114 | { 115 | $requestUri = parse_url($this->getUri(), PHP_URL_PATH); 116 | 117 | // Issue headers to disable browser caching. 118 | $this->setCachingHeaders(); 119 | 120 | $routed = false; 121 | 122 | if ('/' === $requestUri) { 123 | $this->app->onIndexRoute($this); 124 | $routed = true; 125 | } 126 | 127 | if ('/callback' === $requestUri) { 128 | $this->app->onCallbackRoute($this); 129 | $routed = true; 130 | } 131 | 132 | if ('/login' === $requestUri) { 133 | $this->app->onLoginRoute($this); 134 | $routed = true; 135 | } 136 | 137 | if ('/logout' === $requestUri) { 138 | $this->app->onLogoutRoute($this); 139 | $routed = true; 140 | } 141 | 142 | if (false === $routed) { 143 | $this->app->onError404($this); 144 | } 145 | 146 | exit; 147 | } 148 | 149 | /** 150 | * Issue HTTP headers to disable browser caching. 151 | */ 152 | public function setCachingHeaders(): void 153 | { 154 | header('Cache-Control: no-cache, must-revalidate'); 155 | header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); 156 | } 157 | 158 | /** 159 | * Issue a HTTP response code header. 160 | * 161 | * @param int $status The HTTP status code to send. 162 | */ 163 | public function setHttpStatus( 164 | int $status, 165 | ): void { 166 | http_response_code($status); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/ApplicationTemplates.php: -------------------------------------------------------------------------------- 1 | , layout: null|array{name: string, variables: array}} 24 | */ 25 | private array $state = [ 26 | 'sections' => [], 27 | 'section' => null, 28 | 'layout' => null, 29 | ]; 30 | 31 | /** 32 | * ApplicationTemplates constructor. 33 | * 34 | * @param Application $app An instance of our Quickstart Application. 35 | */ 36 | public function __construct( 37 | Application &$app, 38 | ) { 39 | $this->app = &$app; 40 | } 41 | 42 | /** 43 | * Render a template as the browser response, then exit. 44 | * 45 | * @param string $template The name of the template to use. 46 | * @param array $variables Any variables the template should have access to use. 47 | */ 48 | public function render( 49 | string $template, 50 | array $variables = [], 51 | ): void { 52 | $this->state = [ 53 | 'sections' => [], 54 | 'section' => null, 55 | 'layout' => null, 56 | ]; 57 | 58 | while (ob_get_level() >= 1) { 59 | ob_end_clean(); 60 | } 61 | 62 | echo $this->renderTemplate($template, $variables); 63 | exit; 64 | } 65 | 66 | /** 67 | * Define a container layout in which to render a template. 68 | * 69 | * @param string $name The name of the layout template to use. 70 | * @param array $variables Any additional variables the layout template should have access to use. 71 | */ 72 | private function layout( 73 | string $name, 74 | array $variables = [], 75 | ): void { 76 | $this->state['layout'] = [ 77 | 'name' => $name, 78 | 'variables' => $variables, 79 | ]; 80 | } 81 | 82 | /** 83 | * Render a template, and return the content as a string. 84 | * 85 | * @param string $template The name of the template to use. 86 | * @param array $variables Any variables the template should have access to use. 87 | */ 88 | private function renderTemplate( 89 | string $template, 90 | array $variables, 91 | ): string { 92 | // Keep track of the output buffering 'level'. 93 | $level = 0; 94 | 95 | // Resolve the requested template to it's file path: 96 | $templatePath = implode(DIRECTORY_SEPARATOR, [APP_ROOT, 'templates', $template . '.php']); 97 | 98 | // Extract $variables into current scope, for use in template. 99 | extract($variables); 100 | $sdkVersion = \Auth0\SDK\Auth0::VERSION; 101 | 102 | if (! file_exists($templatePath)) { 103 | // @phpstan-ignore-next-line 104 | throw new Exception(sprintf('Template file not found: %s', $template)); 105 | } 106 | 107 | try { 108 | $level = ob_get_level(); 109 | ob_start(); 110 | 111 | include $templatePath; 112 | 113 | $content = ob_get_clean(); 114 | 115 | if (null !== $this->state['layout']) { 116 | $layoutTemplate = $this->state['layout']['name']; 117 | $layoutVariables = array_merge($variables, $this->state['layout']['variables']); 118 | 119 | $this->state['sections']['content'] = $content; 120 | $this->state['layout'] = null; 121 | 122 | $content = $this->renderTemplate($layoutTemplate, $layoutVariables); 123 | } 124 | 125 | if (false !== $content) { 126 | return trim($content); 127 | } 128 | 129 | return ''; 130 | } catch (Throwable $throwable) { 131 | while (ob_get_level() > $level) { 132 | ob_end_clean(); 133 | } 134 | 135 | throw $throwable; 136 | } 137 | } 138 | 139 | /** 140 | * Render a section into the template, if it's content has been set. 141 | * 142 | * @param string $sectionName Name of the section to render into the template. 143 | */ 144 | private function section( 145 | string $sectionName, 146 | ): string { 147 | return $this->state['sections'][$sectionName] ?? ''; 148 | } 149 | 150 | /** 151 | * Start capturing a template block as new section content. 152 | * 153 | * @param string $sectionName Name of the section to begin capturing. 154 | */ 155 | private function start( 156 | string $sectionName, 157 | ): void { 158 | if (null !== $this->state['section']) { 159 | throw new LogicException('Nested sections are not supported.'); 160 | } 161 | 162 | $this->state['section'] = $sectionName; 163 | 164 | ob_start(); 165 | } 166 | 167 | /** 168 | * Stop capturing a previously started template block, and store the section content for use. 169 | */ 170 | private function stop(): void 171 | { 172 | if (null === $this->state['section']) { 173 | throw new LogicException('You must start a section before stopping it.'); 174 | } 175 | 176 | if (array_key_exists($this->state['section'], $this->state['sections'])) { 177 | $this->state['sections'][$this->state['section']] = ''; 178 | } 179 | 180 | $this->state['sections'][$this->state['section']] = ob_get_clean(); 181 | $this->state['section'] = null; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/src/Contract/QuickstartExample.php: -------------------------------------------------------------------------------- 1 | app = &$app; 37 | $this->listener = new ListenerProvider(); 38 | } 39 | 40 | public function onHttpRequestBuilt( 41 | object $event, 42 | ): void { 43 | // Retrieve the built PSR-7 RequestInterface message. 44 | $request = $event->get(); 45 | 46 | // Add a customer header to the outgoing request. 47 | $request = $request->withHeader('X-EXAMPLE-SENDING', 'Just a quickstart demo of changing headers on outgoing API requests.'); 48 | 49 | // Update the PSR-7 RequestInterface object before the SDK dispatches it. 50 | $event->set($request); 51 | } 52 | 53 | public function onHttpResponseReceived( 54 | object $event, 55 | ): void { 56 | // Retrieve the built PSR-7 ResponseInterface message. 57 | $response = $event->get(); 58 | 59 | // Retrieve the PSR-7 RequestInterface that this response was for. 60 | $request = $event->getRequest(); 61 | $requestUri = $request->getUri(); 62 | 63 | // Overwrite the body of responses from example.us.auth0/somewhere. 64 | if ('example.us.auth0.com' === $requestUri->getHost() 65 | && '/somewhere' === $requestUri->getPath()) { 66 | $response = $response->withBody(Utils::streamFor('new content for the response')); 67 | } 68 | 69 | // Echo the respond object and terminate the request. 70 | // @phpstan-ignore-next-line 71 | if (self::ECHO_HTTP_RESPONSE) { 72 | echo '
';
73 |             print_r($response->getBody()->__toString());
74 |             exit;
75 |         }
76 | 
77 |         // Update the PSR-7 ResponseInterface object before the SDK uses it.
78 |         $event->set($response);
79 |     }
80 | 
81 |     public function setup(): self
82 |     {
83 |         // Register our PSR-14 ListenerProvider.
84 |         $this->app->getConfiguration()->setEventListenerProvider($this->listener);
85 | 
86 |         // Register the events we want to listen for.
87 |         $this->listener->on(HttpRequestBuilt::class, function (object $event): void {
88 |             $this->onHttpRequestBuilt($event);
89 |         });
90 |         $this->listener->on(HttpResponseReceived::class, function (object $event): void {
91 |             $this->onHttpResponseReceived($event);
92 |         });
93 | 
94 |         return $this;
95 |     }
96 | }
97 | 


--------------------------------------------------------------------------------
/app/src/Example/Management.php:
--------------------------------------------------------------------------------
  1 | app = &$app;
 27 |     }
 28 | 
 29 |     /**
 30 |      * Query the users endpoint and retrieve details about a user.
 31 |      *
 32 |      * @param ManagementAPI $api An instance of the ManagementAPI to issue the request through.
 33 |      * @param string        $sub The user identifier to query.
 34 |      *
 35 |      * @return null|array
 36 |      */
 37 |     public function getProfile(
 38 |         ManagementAPI $api,
 39 |         string $sub,
 40 |     ): ?array {
 41 |         $response = $api->users()->get($sub);
 42 | 
 43 |         if (! HttpResponse::wasSuccessful($response)) {
 44 |             exit('Management API request failed. Unable to get user.');
 45 |         }
 46 | 
 47 |         return HttpResponse::decodeContent($response);
 48 |     }
 49 | 
 50 |     public function onIndexRoute(
 51 |         ApplicationRouter $router,
 52 |         ?stdClass $session,
 53 |     ): ?bool {
 54 |         if ($session instanceof stdClass) {
 55 |             $api = $this->app->getSdk()->management();
 56 | 
 57 |             if ('POST' === $router->getMethod()) {
 58 |                 $this->updateProfile($api, $session->user['sub']);
 59 |             }
 60 | 
 61 |             $profile = $this->getProfile($api, $session->user['sub']);
 62 | 
 63 |             $this->app->getTemplate()->render('management', [
 64 |                 'session' => $session,
 65 |                 'router' => $router,
 66 |                 'cookies' => $_COOKIE,
 67 |                 'managementUserProfile' => $profile,
 68 |             ]);
 69 | 
 70 |             return true;
 71 |         }
 72 | 
 73 |         return null;
 74 |     }
 75 | 
 76 |     /**
 77 |      * Update the user_metadata of a user, as a demonstration of the process.
 78 |      *
 79 |      * @param ManagementAPI $api An instance of the ManagementAPI to issue the request through.
 80 |      * @param string        $sub The user identifier to update metadata for.
 81 |      *
 82 |      * @return null|array
 83 |      */
 84 |     public function updateProfile(
 85 |         ManagementAPI $api,
 86 |         string $sub,
 87 |     ): ?array {
 88 |         $response = $api->users()->update($sub, [
 89 |             'user_metadata' => [
 90 |                 'quickstart_example' => 'Updated ' . date(DATE_RFC2822, time()) . ' using the auth-PHP SDK quickstart!',
 91 |             ],
 92 |         ]);
 93 | 
 94 |         if (! HttpResponse::wasSuccessful($response)) {
 95 |             exit('Management API request failed. Unable to update user.');
 96 |         }
 97 | 
 98 |         return HttpResponse::decodeContent($response);
 99 |     }
100 | 
101 |     public function setup(): self
102 |     {
103 |         // Register our example hook to override the default quickstart onLoginRoute behavior:
104 |         $this->app->hook('onIndexRoute', fn (\Auth0\Quickstart\ApplicationRouter $router, ?stdClass $session): ?bool => $this->onIndexRoute($router, $session));
105 | 
106 |         return $this;
107 |     }
108 | }
109 | 


--------------------------------------------------------------------------------
/app/src/Example/PasswordlessMagic.php:
--------------------------------------------------------------------------------
  1 | app = &$app;
 26 |     }
 27 | 
 28 |     public function onCallbackRoute(
 29 |         ApplicationRouter $router,
 30 |     ): bool {
 31 |         // @phpstan-ignore-next-line
 32 |         $passwordlessState = $_GET['passwordless'] ?? null;
 33 | 
 34 |         // @phpstan-ignore-next-line
 35 |         $hash = $_GET['hash'] ?? null;
 36 | 
 37 |         if ('complete' === $passwordlessState) {
 38 |             $this->app->getSdk()->exchange($router->getUri('/', ''));
 39 |             $router->redirect($router->getUri('/', ''));
 40 |         }
 41 | 
 42 |         if (null === $hash) {
 43 |             $this->app->getTemplate()->render('passwordless-magic-callback');
 44 | 
 45 |             return true;
 46 |         }
 47 | 
 48 |         $params = explode('&', urldecode($hash));
 49 | 
 50 |         foreach ($params as $index => $param) {
 51 |             [$key, $value] = explode('=', $param);
 52 |             $params[$key] = $value;
 53 |             unset($params[$index]);
 54 |         }
 55 | 
 56 |         $accessToken = $params['access_token'] ?? null;
 57 |         $scope = $params['scope'] ?? null;
 58 |         $expiresIn = $params['expires_in'] ?? null;
 59 | 
 60 |         if (null === $accessToken || null === $scope || null === $expiresIn) {
 61 |             $router->redirect($router->getUri('/', ''));
 62 | 
 63 |             return true;
 64 |         }
 65 | 
 66 |         // Authentication was successful. For some applications, this access token may be enough for your needs.
 67 |         // For the purposes for this quickstart, we'll redirect back to Auth0 using 'silent authentication' (prompt=none) to get an Id Token.
 68 |         $router->redirect($this->app->getSdk()->login($router->getUri('/callback', 'passwordless=complete'), ['prompt' => 'none']));
 69 | 
 70 |         return true;
 71 |     }
 72 | 
 73 |     public function onLoginRoute(
 74 |         ApplicationRouter $router,
 75 |     ): bool {
 76 |         $startedPasswordless = false;
 77 | 
 78 |         if ('POST' === $router->getMethod()) {
 79 |             // @phpstan-ignore-next-line
 80 |             $email = filter_var($_POST['passwordless_email'], FILTER_SANITIZE_EMAIL);
 81 | 
 82 |             if (false === $email) {
 83 |                 $router->redirect($router->getUri('/', ''));
 84 | 
 85 |                 return true;
 86 |             }
 87 | 
 88 |             $response = $this->app->getSdk()->authentication()->emailPasswordlessStart($email, 'link', [
 89 |                 'redirect_uri' => $router->getUri('/callback', ''),
 90 |                 'scope' => $this->app->getConfiguration()->formatScope(),
 91 |             ]);
 92 | 
 93 |             if (! HttpResponse::wasSuccessful($response)) {
 94 |                 $response = HttpResponse::decodeContent($response);
 95 | 
 96 |                 $error = $response['error'] ?? null;
 97 | 
 98 |                 if ('bad.connection' === $error) {
 99 |                     $error = 'You must enable the "email" passwordless connection in your Auth0 Dashboard for this application first.';
100 |                 } else {
101 |                     $error = $response['error_description'] ?? $response['error'] ?? 'Encountered API error when attempting to start passwordless login.';
102 |                 }
103 | 
104 |                 // @phpstan-ignore-next-line
105 |                 throw new Exception('API Error: ' . (string) $error);
106 |             }
107 | 
108 |             $startedPasswordless = true;
109 |         }
110 | 
111 |         // Display login page, prompting for email address:
112 |         $this->app->getTemplate()->render('passwordless-magic-login', [
113 |             'startedPasswordless' => $startedPasswordless,
114 |             'router' => $router,
115 |             'cookies' => $_COOKIE,
116 |         ]);
117 | 
118 |         return true;
119 |     }
120 | 
121 |     public function setup(): self
122 |     {
123 |         // Register our quickstart example hook to override the default onLoginRoute behavior:
124 |         $this->app->hook('onLoginRoute', fn (\Auth0\Quickstart\ApplicationRouter $router): bool => $this->onLoginRoute($router));
125 |         $this->app->hook('onCallbackRoute', fn (\Auth0\Quickstart\ApplicationRouter $router): bool => $this->onCallbackRoute($router));
126 | 
127 |         return $this;
128 |     }
129 | }
130 | 


--------------------------------------------------------------------------------
/app/templates/_layout.php:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 | 
 5 |     
 6 |     
 7 |     
 8 | 
 9 |     Auth0 PHP Quickstart for Web Apps
10 | 
11 |     
24 | 
25 | 
26 | 
27 |     
28 | 38 |
39 |
40 | 63 |
64 | 65 |
66 |
67 |
68 | section('hero')?> 69 |
70 |
71 |
72 |
73 |
74 | 75 |
76 |
77 | section('body')?> 78 |
79 |
80 | 81 |
82 |
83 |

This sample application is importing Auth0-PHP v.

84 |
85 |
86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/templates/error.php: -------------------------------------------------------------------------------- 1 | layout('_layout'); ?> 2 | 3 | start('hero') ?> 4 | 5 | Error 6 | PHP Quickstart 7 | 10 | 11 | 12 |

13 | Oops! 14 | 15 |

16 | 17 |

Raised on line of .

18 | stop() ?> 19 | 20 | start('body') ?> 21 |
22 | 34 | 35 |
36 | 37 |
38 |
39 |
40 |
41 | 44 |
45 |
46 |

Local Development Tip

47 |
48 |

You appear to be running this quickstart on your local machine. Please ensure you are being consistent in your use of localhost and/or 127.0.0.1, as this is the most common pitfall for new developers hitting errors. Make sure the Quickstart and Auth0 are configured to use the same address to avoid authentication errors, and that you're using the same address in your browser to ensure cookies can be set properly.

49 |
50 |
51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 |
59 |
60 |

Cookies

61 | 62 | 63 |

The contents of PHP's $_COOKIE global.

64 | 65 |
66 | 67 |
68 | 69 | $data): ?> 70 | 71 | 72 | 73 |
74 | 75 | 76 |
77 | 78 | 79 | 80 |
81 | 82 | 83 |
84 | 85 | 86 | 87 |

Nothing to show.

88 | 89 |
90 |
91 |
92 |
93 |
94 |
95 | stop() ?> 96 | -------------------------------------------------------------------------------- /app/templates/logged-in.php: -------------------------------------------------------------------------------- 1 | layout('_layout'); ?> 2 | 3 | start('hero') ?> 4 | 5 | Logged In 6 | PHP Quickstart 7 | 10 | 11 | 12 |

13 | Welcome 14 | 15 | user['picture'])): ?> 16 | 17 | 18 | 19 | 20 | 21 | user['email'] ?> 22 | 23 |

24 | 25 |

You successfully authenticated as user['sub'] ?>.

26 | stop() ?> 27 | 28 | start('body') ?> 29 |
30 | 60 | 61 |
62 |
63 |
64 |
65 |
66 |

User Information

67 |

Retrieve these details using Auth0::getCredentials() or Auth0::getUser().

68 |
69 | 70 |
71 | user as $key => $data): ?> 72 | 73 | 74 | 75 |
76 | 77 | 78 |
79 | 80 | 81 | 82 |
83 | 84 | 85 |
86 | 87 | 88 |
89 |
90 |
91 |
92 | 93 |
94 |
95 |
96 |
97 |

Credentials

98 |

Retrieve these using Auth0::getCredentials(), or Auth0::getIdToken(), Auth0::getAccessToken(), Auth0::getAccessTokenScope(), Auth0::getAccessTokenExpiration(), and Auth0::getRefreshToken() respectively.

99 |
100 | 101 |
102 |
103 | 104 | 105 |
106 | 107 |
108 | 109 | 110 |
111 | 112 |
113 | 114 | 115 |
116 | 117 |
118 | 119 | 120 |
121 | 122 |
123 | 124 | 125 |
126 |
127 |
128 |
129 |
130 | 131 |
132 |
133 |
134 |
135 |

Cookies

136 | 137 | 138 |

The contents of PHP's $_COOKIE global.

139 | 140 |
141 | 142 |
143 | 144 | $data): ?> 145 | 146 | 147 | 148 |
149 | 150 | 151 |
152 | 153 | 154 | 155 |
156 | 157 | 158 |
159 | 160 | 161 | 162 |

Nothing to show.

163 | 164 |
165 |
166 |
167 |
168 |
169 |
170 | stop() ?> 171 | -------------------------------------------------------------------------------- /app/templates/logged-out.php: -------------------------------------------------------------------------------- 1 | layout('_layout'); ?> 2 | 3 | start('hero') ?> 4 | 5 | Logged Out 6 | PHP Quickstart 7 | 10 | 11 | 12 |

13 | Welcome, 14 | Guest 15 |

16 | 17 |

You are not authenticated.

18 | stop() ?> 19 | 20 | start('body') ?> 21 |
22 | 34 | 35 |
36 |
37 |
38 |
39 |
40 |

Cookies

41 | 42 | 43 |

The contents of PHP's $_COOKIE global.

44 | 45 |
46 | 47 |
48 | 49 | $data): ?> 50 | 51 | 52 | 53 |
54 | 55 | 56 |
57 | 58 | 59 | 60 |
61 | 62 | 63 |
64 | 65 | 66 | 67 |

Nothing to show.

68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 | stop() ?> 76 | -------------------------------------------------------------------------------- /app/templates/management.php: -------------------------------------------------------------------------------- 1 | layout('_layout'); ?> 2 | 3 | start('hero') ?> 4 | 5 | Logged In 6 | PHP Quickstart 7 | 10 | 11 | 12 |

13 | Welcome 14 | 15 | user['picture'])): ?> 16 | 17 | 18 | 19 | 20 | 21 | user['email'] ?> 22 | 23 |

24 | 25 |

You successfully authenticated as user['sub'] ?>.

26 | stop() ?> 27 | 28 | start('body') ?> 29 |
30 | 60 | 61 |
62 |
63 |
64 |
65 |
66 |

Management API GET /api/v2/users/{id}

67 |
68 | 69 |
70 | 71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 |
80 |

Management API PATCH /api/v2/users/{id}

81 |
82 | 83 |
84 | 85 |
86 |
87 |
88 |
89 | 90 |
91 |
92 |
93 |
94 |

User Information

95 |

Retrieve these details using Auth0::getCredentials() or Auth0::getUser().

96 |
97 | 98 |
99 | user as $key => $data): ?> 100 | 101 | 102 | 103 |
104 | 105 | 106 |
107 | 108 | 109 | 110 |
111 | 112 | 113 |
114 | 115 | 116 |
117 |
118 |
119 |
120 | 121 |
122 |
123 |
124 |
125 |

Credentials

126 |

Retrieve these using Auth0::getCredentials(), or Auth0::getIdToken(), Auth0::getAccessToken(), Auth0::getAccessTokenScope(), Auth0::getAccessTokenExpiration(), and Auth0::getRefreshToken() respectively.

127 |
128 | 129 |
130 |
131 | 132 | 133 |
134 | 135 |
136 | 137 | 138 |
139 | 140 |
141 | 142 | 143 |
144 | 145 |
146 | 147 | 148 |
149 | 150 |
151 | 152 | 153 |
154 |
155 |
156 |
157 |
158 | 159 |
160 |
161 |
162 |
163 |

Cookies

164 | 165 | 166 |

The contents of PHP's $_COOKIE global.

167 | 168 |
169 | 170 |
171 | 172 | $data): ?> 173 | 174 | 175 | 176 |
177 | 178 | 179 |
180 | 181 | 182 | 183 |
184 | 185 | 186 |
187 | 188 | 189 | 190 |

Nothing to show.

191 | 192 |
193 |
194 |
195 |
196 |
197 |
198 | stop() ?> 199 | -------------------------------------------------------------------------------- /app/templates/passwordless-magic-callback.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/templates/passwordless-magic-login.php: -------------------------------------------------------------------------------- 1 | layout('_layout'); ?> 2 | 3 | start('hero') ?> 4 | 5 | Logged Out 6 | PHP Quickstart 7 | 10 | 11 | 12 |

13 | Welcome, 14 | Guest 15 |

16 | 17 |

You are not authenticated.

18 | stop() ?> 19 | 20 | start('body') ?> 21 |
22 | 43 | 44 |
45 | 46 |
47 |
48 |
49 |
50 | 53 |
54 |
55 |

Email Sent!

56 |
57 |

Check your inbox and click the link to continue logging in.

58 |
59 |
60 |
61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 |

Passwordless Login

70 |
71 | 72 |
73 |
74 | 75 | 76 |
77 |
78 | 79 |
80 |
81 | 82 |
83 |
84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 |

Cookies

93 | 94 | 95 |

The contents of PHP's $_COOKIE global.

96 | 97 |
98 | 99 |
100 | 101 | $data): ?> 102 | 103 | 104 | 105 |
106 | 107 | 108 |
109 | 110 | 111 | 112 |
113 | 114 | 115 |
116 | 117 | 118 | 119 |

Nothing to show.

120 | 121 |
122 |
123 |
124 |
125 |
126 |
127 | stop() ?> 128 | -------------------------------------------------------------------------------- /app/tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /app/tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Expectations 19 | |-------------------------------------------------------------------------- 20 | | 21 | | When you're writing tests, you often need to check that values meet certain conditions. The 22 | | "expect()" function gives you access to a set of "expectations" methods that you can use 23 | | to assert different things. Of course, you may extend the Expectation API at any time. 24 | | 25 | */ 26 | 27 | expect()->extend('toBeOne', function () { 28 | return $this->toBe(1); 29 | }); 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Functions 34 | |-------------------------------------------------------------------------- 35 | | 36 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 37 | | project that you don't want to repeat in every file. Here you can also expose helpers as 38 | | global functions to help you to reduce the number of lines of code in your test files. 39 | | 40 | */ 41 | 42 | function something() 43 | { 44 | // .. 45 | } 46 | -------------------------------------------------------------------------------- /app/tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | --------------------------------------------------------------------------------