Repository: DutchCodingCompany/filament-socialite Branch: main Commit: 49cfcb8c7175 Files: 68 Total size: 134.6 KB Directory structure: gitextract_wtq0hz3c/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── config.yml │ ├── dependabot.yml │ └── workflows/ │ ├── dependabot-auto-merge.yml │ ├── php-cs-fixer.yml │ ├── phpstan.yml │ ├── run-tests.yml │ └── update-changelog.yml ├── .gitignore ├── .php-cs-fixer.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── UPGRADE.md ├── bin/ │ └── upgrade-v2 ├── composer.json ├── config/ │ └── filament-socialite.php ├── database/ │ └── migrations/ │ └── create_socialite_users_table.php.stub ├── package.json ├── phpstan-baseline.neon ├── phpstan.neon.dist ├── phpunit.xml ├── rector.php ├── resources/ │ ├── css/ │ │ └── plugin.css │ ├── dist/ │ │ └── plugin.css │ ├── lang/ │ │ └── en/ │ │ └── auth.php │ └── views/ │ ├── .gitkeep │ └── components/ │ └── buttons.blade.php ├── routes/ │ └── web.php ├── src/ │ ├── Events/ │ │ ├── InvalidState.php │ │ ├── Login.php │ │ ├── Registered.php │ │ ├── RegistrationNotEnabled.php │ │ ├── SocialiteUserConnected.php │ │ └── UserNotAllowed.php │ ├── Exceptions/ │ │ ├── GuardNotStateful.php │ │ ├── ImplementationException.php │ │ ├── InvalidCallbackPayload.php │ │ └── ProviderNotConfigured.php │ ├── FilamentSocialitePlugin.php │ ├── FilamentSocialiteServiceProvider.php │ ├── Http/ │ │ ├── Controllers/ │ │ │ └── SocialiteLoginController.php │ │ └── Middleware/ │ │ └── PanelFromUrlQuery.php │ ├── Models/ │ │ ├── Contracts/ │ │ │ └── FilamentSocialiteUser.php │ │ └── SocialiteUser.php │ ├── Provider.php │ ├── Traits/ │ │ ├── Callbacks.php │ │ ├── CanBeHidden.php │ │ ├── Models.php │ │ └── Routes.php │ └── View/ │ └── Components/ │ └── Buttons.php ├── tailwind.config.js └── tests/ ├── Fixtures/ │ ├── TestSocialiteUser.php │ ├── TestTeam.php │ ├── TestTenantUser.php │ ├── TestUser.php │ ├── change_nullable_password_on_users_table.php │ ├── create_socialite_users_table.php │ ├── create_team_user_table.php │ └── create_teams_table.php ├── SocialiteLoginAuthorizationTest.php ├── SocialiteLoginTest.php ├── SocialiteStatelessLoginTest.php ├── SocialiteTenantLoginTest.php └── TestCase.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_size = 4 indent_style = space end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.{yml,yaml}] indent_size = 2 ================================================ FILE: .gitattributes ================================================ # Path-based git attributes # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html # Ignore all test and documentation with "export-ignore". /.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore /phpunit.xml.dist export-ignore /art export-ignore /docs export-ignore /tests export-ignore /.editorconfig export-ignore /.php_cs.dist.php export-ignore /psalm.xml export-ignore /psalm.xml.dist export-ignore /testbench.yaml export-ignore /UPGRADING.md export-ignore /phpstan.neon.dist export-ignore /phpstan-baseline.neon export-ignore ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Report a problem you're experiencing body: - type: markdown attributes: value: | Before opening a bug report, please search the existing issues (both open and closed). --- Thank you for taking the time to file a bug report. To address this bug as fast as possible, we need some information. - type: input id: plugin-version attributes: label: Plugin Version description: Please provide the version of filament socialite installed in your project. placeholder: v2.0.0 validations: required: true - type: input id: filament-version attributes: label: Filament Version description: Please provide the full filament version of your project. placeholder: v3.0.0 validations: required: true - type: input id: laravel-version attributes: label: Laravel Version description: Please provide the full Laravel version of your project. placeholder: v10.0.0 validations: required: true - type: input id: livewire-version attributes: label: Livewire Version description: Please provide the full Livewire version of your project, if applicable. placeholder: v3.0.0 - type: input id: php-version attributes: label: PHP Version description: Please provide the full PHP version of your server. placeholder: PHP 8.3.0 validations: required: true - type: textarea id: description attributes: label: Problem description description: What happened when you experienced the problem? validations: required: true - type: textarea id: expectation attributes: label: Expected behavior description: What did you expect to happen instead? validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce description: Which steps do we need to take to reproduce the problem? Any code examples need to be **as short as possible**, remove any code that is unrelated to the bug. validations: required: true - type: textarea id: logs attributes: label: Relevant log output description: If applicable, provide relevant log output. No need for backticks here. render: shell ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask a question url: https://discord.com/channels/883083792112300104/962299008259342366 about: Ask the community for help - name: Request a feature url: https://discord.com/channels/883083792112300104/962299008259342366 about: Share ideas for new features - name: Report a security issue url: https://github.com/DutchCodingCompany/filament-socialite/security/policy about: Learn how to notify us for sensitive bugs ================================================ FILE: .github/dependabot.yml ================================================ # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" labels: - "dependencies" ================================================ FILE: .github/workflows/dependabot-auto-merge.yml ================================================ name: dependabot-auto-merge on: pull_request_target permissions: pull-requests: write contents: write jobs: dependabot: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v3.0.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Auto-merge Dependabot PRs for semver-minor updates if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Auto-merge Dependabot PRs for semver-patch updates if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} ================================================ FILE: .github/workflows/php-cs-fixer.yml ================================================ name: Check & fix styling on: [push] jobs: php-cs-fixer: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: ref: ${{ github.head_ref }} - name: Run PHP CS Fixer uses: docker://oskarstark/php-cs-fixer-ga with: args: --config=.php-cs-fixer.php --allow-risky=yes - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: Fix styling ================================================ FILE: .github/workflows/phpstan.yml ================================================ name: PHPStan on: push: paths: - '**.php' - .github/workflows/phpstan.yml - phpstan.neon.dist pull_request: jobs: phpstan: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest] php: [8.4, 8.3, 8.2] laravel: ['11.*', '12.*', '13.*'] stability: [prefer-stable] include: - laravel: 11.* testbench: 9.* - laravel: 12.* testbench: 10.* - laravel: 13.* testbench: 11.* exclude: - laravel: 13.* php: 8.2 name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo coverage: none - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: List Installed Dependencies run: composer show -D - name: Run PHPStan run: vendor/bin/phpstan --error-format=github ================================================ FILE: .github/workflows/run-tests.yml ================================================ name: run-tests on: push: paths: - '**.php' - .github/workflows/run-tests.yml - phpunit.xml.dist - composer.json - composer.lock pull_request: jobs: test: runs-on: ${{ matrix.os }} timeout-minutes: 5 strategy: fail-fast: false matrix: os: [ubuntu-latest] php: [8.4, 8.3, 8.2] laravel: ['11.*', '12.*', '13.*'] stability: [prefer-stable] include: - laravel: 11.* testbench: 9.* - laravel: 12.* testbench: 10.* - laravel: 13.* testbench: 11.* exclude: - laravel: 13.* php: 8.2 name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo coverage: none - name: Setup problem matchers run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: List Installed Dependencies run: composer show -D - name: Execute tests run: vendor/bin/phpunit tests ================================================ FILE: .github/workflows/update-changelog.yml ================================================ name: "Update Changelog" on: release: types: [released] jobs: update: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: ref: main - name: Update Changelog uses: stefanzweifel/changelog-updater-action@v1 with: latest-version: ${{ github.event.release.name }} release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG uses: stefanzweifel/git-auto-commit-action@v7 with: branch: main commit_message: Update CHANGELOG file_pattern: CHANGELOG.md ================================================ FILE: .gitignore ================================================ .idea .php_cs .php_cs.cache .phpunit.result.cache build composer.lock package-lock.json coverage docs phpstan.neon testbench.yaml vendor node_modules .php-cs-fixer.cache ================================================ FILE: .php-cs-fixer.php ================================================ true, 'array_syntax' => ['syntax' => 'short'], 'binary_operator_spaces' => [ 'default' => 'single_space', // 'operators' => ['=>' => null], // single space makes code look more coherent in style. But sometimes it is not beter, in that case, manually override. ], 'blank_line_after_namespace' => true, 'blank_line_after_opening_tag' => true, 'blank_line_before_statement' => [ 'statements' => ['return'], ], 'braces' => true, 'cast_spaces' => true, 'class_attributes_separation' => [ 'elements' => [ 'const' => 'only_if_meta', 'method' => 'one', 'property' => 'one', 'trait_import' => 'none', ], ], 'class_definition' => [ 'multi_line_extends_each_single_line' => true, 'single_item_single_line' => true, 'single_line' => true, ], 'concat_space' => [ 'spacing' => 'none', ], 'constant_case' => ['case' => 'lower'], 'declare_equal_normalize' => true, 'elseif' => true, 'encoding' => true, 'full_opening_tag' => true, 'fully_qualified_strict_types' => false, // added by Shift 'function_declaration' => true, 'function_typehint_space' => true, 'general_phpdoc_tag_rename' => true, 'heredoc_to_nowdoc' => true, 'include' => true, 'increment_style' => ['style' => 'post'], 'indentation_type' => true, 'linebreak_after_opening_tag' => true, 'line_ending' => true, 'lowercase_cast' => true, 'lowercase_keywords' => true, 'lowercase_static_reference' => true, // added from Symfony 'magic_method_casing' => true, // added from Symfony 'magic_constant_casing' => true, 'method_argument_space' => [ 'on_multiline' => 'ignore', ], 'multiline_whitespace_before_semicolons' => [ 'strategy' => 'no_multi_line', ], 'native_function_casing' => true, 'no_alias_functions' => true, 'no_extra_blank_lines' => [ 'tokens' => [ 'extra', 'throw', 'use', 'switch', 'case', 'default', ], ], 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, 'no_closing_tag' => true, 'no_empty_phpdoc' => true, 'no_empty_statement' => true, 'no_leading_import_slash' => true, 'no_leading_namespace_whitespace' => true, 'no_mixed_echo_print' => [ 'use' => 'echo', ], 'no_multiline_whitespace_around_double_arrow' => true, 'no_short_bool_cast' => true, 'no_singleline_whitespace_before_semicolons' => true, 'no_spaces_after_function_name' => true, 'no_spaces_around_offset' => [ 'positions' => [ 'inside', 'outside', ], ], 'no_spaces_inside_parenthesis' => true, 'no_trailing_comma_in_list_call' => true, 'no_trailing_comma_in_singleline_array' => true, 'no_trailing_whitespace' => true, 'no_trailing_whitespace_in_comment' => true, 'no_unneeded_control_parentheses' => [ 'statements' => [ 'break', 'clone', 'continue', 'echo_print', 'return', 'switch_case', 'yield', ], ], 'no_unreachable_default_argument_value' => true, 'no_useless_return' => true, 'no_whitespace_before_comma_in_array' => true, 'no_whitespace_in_blank_line' => true, 'normalize_index_brace' => true, 'not_operator_with_successor_space' => true, 'object_operator_without_whitespace' => true, 'ordered_imports' => [ 'sort_algorithm' => 'alpha', 'imports_order' => [ 'class', 'function', 'const', ], ], 'psr_autoloading' => true, 'phpdoc_indent' => true, 'phpdoc_inline_tag_normalizer' => true, 'phpdoc_no_access' => true, 'phpdoc_no_package' => true, 'phpdoc_no_useless_inheritdoc' => true, 'phpdoc_scalar' => true, 'phpdoc_single_line_var_spacing' => true, 'phpdoc_summary' => false, 'phpdoc_to_comment' => false, // override to preserve user preference 'phpdoc_tag_type' => true, 'phpdoc_trim' => true, 'phpdoc_types' => true, 'phpdoc_var_without_name' => true, 'self_accessor' => true, 'short_scalar_cast' => true, 'simplified_null_return' => false, // disabled as "risky" 'single_blank_line_at_eof' => true, 'single_blank_line_before_namespace' => true, 'single_class_element_per_statement' => [ 'elements' => [ 'const', 'property', ], ], 'single_import_per_statement' => true, 'single_line_after_imports' => true, 'single_line_comment_style' => [ 'comment_types' => ['hash'], ], 'single_quote' => true, 'space_after_semicolon' => true, 'standardize_not_equals' => true, 'switch_case_semicolon_to_colon' => true, 'switch_case_space' => true, 'ternary_operator_spaces' => true, 'trailing_comma_in_multiline' => [ 'elements' => [ 'arrays', 'parameters', ], ], 'trim_array_spaces' => true, 'types_spaces' => [ 'space' => 'single', ], 'unary_operator_spaces' => true, 'visibility_required' => [ 'elements' => [ 'method', 'property', 'const', ], ], 'whitespace_after_comma_in_array' => true, // DCC 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], 'simplified_if_return' => true, 'method_chaining_indentation' => true, ]; $finder = Finder::create() ->in([ __DIR__ . '/src', __DIR__ . '/tests', ]) ->name('*.php') ->notName('*.blade.php') ->ignoreDotFiles(true) ->ignoreVCS(true); return (new Config) ->setFinder($finder) ->setRules($rules) ->setRiskyAllowed(true) ->setUsingCache(true); ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to `filament-socialite` will be documented in this file. ## [3.2.1 - 2026-04-14](https://github.com/DutchCodingCompany/filament-socialite/compare/3.2.0...3.2.1) ## What's Changed * fix: update state retrieval method in PanelFromUrlQuery middleware by @fului in https://github.com/DutchCodingCompany/filament-socialite/pull/153 * Bump dependabot/fetch-metadata from 2.5.0 to 3.0.0 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/151 ## [3.2.0 - 2026-03-19](https://github.com/DutchCodingCompany/filament-socialite/compare/3.1.0...3.2.0) ## What's Changed * Add return type declarations to migration methods by @phh in https://github.com/DutchCodingCompany/filament-socialite/pull/149 * Support laravel 13 https://github.com/DutchCodingCompany/filament-socialite/pull/150 ## [3.1.0 - 2026-01-19](https://github.com/DutchCodingCompany/filament-socialite/compare/3.0.1...3.1.0) ## What's Changed - Add support for filament v5 by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/147 - Bump dependabot/fetch-metadata from 2.4.0 to 2.5.0 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/145 ## [3.0.1 - 2025-12-16](https://github.com/DutchCodingCompany/filament-socialite/compare/3.0.0...3.0.1) ## What's Changed - Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/135 - Bump stefanzweifel/git-auto-commit-action from 6 to 7 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/139 - Bump actions/checkout from 5 to 6 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/141 - Add foreign key constraint with cascade behaviour. by @chrillep in https://github.com/DutchCodingCompany/filament-socialite/pull/143 - **NOTE**: this will not change existing `socialite_users` tables. Please **consider adding** the constraint in a separate migration yourself ## [3.0.0 - 2025-08-11](https://github.com/DutchCodingCompany/filament-socialite/compare/2.4.0...3.0.0) - Tag major version - BREAKING CHANGE: Implement fix for slug issue https://github.com/DutchCodingCompany/filament-socialite/issues/127 The package now uses `path` instead of `id` as default prefix as it should have done. In order to revert to previous behaviour, use slug to override the behaviour: ```php ->plugin( FilamentSocialitePlugin::make() ->slug('admin') // change this to the panel's ID // other config for plugin ) ``` ## [3.0.0-beta3 - 2025-07-18](https://github.com/DutchCodingCompany/filament-socialite/compare/3.0.0-beta2...3.0.0-beta3) ## What's Changed * Include compiled styles ## [3.0.0-beta2 - 2025-06-23](https://github.com/DutchCodingCompany/filament-socialite/compare/3.0.0-alpha1...3.0.0-beta2) ## What's Changed * BREAKING CHANGE: Implement fix for slug issue https://github.com/DutchCodingCompany/filament-socialite/issues/127 The package now uses `path` instead of `id` as default prefix as it should have done. In order to revert to previous behaviour, use slug to override the behaviour: ```php ->plugin( FilamentSocialitePlugin::make() ->slug('admin') // change this to the panel's ID // other config for plugin ) ``` ## [3.0.0-alpha1 - 2025-06-05](https://github.com/DutchCodingCompany/filament-socialite/compare/2.4.0...3.0.0-alpha1) / [3.0.0-beta1 - 2025-06-05](https://github.com/DutchCodingCompany/filament-socialite/compare/2.4.0...3.0.0-beta1) ## What's Changed * Filament V4 support by @erikgaal in https://github.com/DutchCodingCompany/filament-socialite/pull/131 ## [2.4.0 - 2025-02-25](https://github.com/DutchCodingCompany/filament-socialite/compare/2.3.1...2.4.0) ## What's Changed * Laravel 12.x Compatibility by @laravel-shift in https://github.com/DutchCodingCompany/filament-socialite/pull/125 ## [2.3.1 - 2025-02-06](https://github.com/DutchCodingCompany/filament-socialite/compare/2.3.0...2.3.1) ## What's Changed * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/123 * Add data to events by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/124 ## [2.3.0 - 2024-11-29](https://github.com/DutchCodingCompany/filament-socialite/compare/2.2.1...2.3.0) ## What's Changed * Add option to hide providers by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/122 * Add support for php 8.4 ## [2.2.1 - 2024-07-17](https://github.com/DutchCodingCompany/filament-socialite/compare/2.2.0...2.2.1) ## What's Changed * Revert model property changes by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/110 ## [2.2.0 - 2024-07-15](https://github.com/DutchCodingCompany/filament-socialite/compare/2.1.1...2.2.0) ## What's Changed * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/106 * Add new callback route for stateless OAuth flows by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/105 ## [2.1.1 - 2024-06-21](https://github.com/DutchCodingCompany/filament-socialite/compare/2.1.0...2.1.1) ## What's Changed * Improve Socialite driver typings + callable typings by @juliangums in https://github.com/DutchCodingCompany/filament-socialite/pull/103 ## New Contributors * @juliangums made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/103 ## [2.1.0 - 2024-06-21](https://github.com/DutchCodingCompany/filament-socialite/compare/2.0.0...2.1.0) * Add Authorization Callback by @petecoop in https://github.com/DutchCodingCompany/filament-socialite/pull/100 ## [2.0.0 - 2024-06-04](https://github.com/DutchCodingCompany/filament-socialite/compare/1.5.0...2.0.0) * **Please check the revised [README.md](https://github.com/DutchCodingCompany/filament-socialite/blob/main/README.md) and [UPGRADE.md](https://github.com/DutchCodingCompany/filament-socialite/blob/main/UPGRADE.md)! Many functions have been renamed.** * Refactor package for better consistency with Filament code standards https://github.com/DutchCodingCompany/filament-socialite/pull/90 ## [1.5.0 - 2024-06-04](https://github.com/DutchCodingCompany/filament-socialite/compare/1.4.1...1.5.0) * Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/89 * Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/91 * Compatible with Stateless Authentication by @LittleHans8 in https://github.com/DutchCodingCompany/filament-socialite/pull/96 ## [1.4.1 - 2024-03-20](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.4.0...1.4.1) * Provide oauth user to login event by @dcc-bjorn in https://github.com/DutchCodingCompany/filament-socialite/pull/88 ## [1.4.0 - 2024-03-12](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.3.1...1.4.0) * Laravel 11 support by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/87 ## [1.3.1 - 2024-03-05](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.3.0...1.3.1) * Add $provider as required by the callback by @phh in https://github.com/DutchCodingCompany/filament-socialite/pull/83 * Never use SPA mode for oauth links + spacing by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/85 ## [1.3.0 - 2024-03-01](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.2.0...1.3.0) * Update CHANGELOG.md by @bramr94 in https://github.com/DutchCodingCompany/filament-socialite/pull/70 * Add socialite test by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/78 * Improve actions by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/79 * Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/51 * feature: allow socialite user model customization by @kykurniawan in https://github.com/DutchCodingCompany/filament-socialite/pull/72 * Add registration enabled callable by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/80 * Multi-tenancy support by @bramr94 in https://github.com/DutchCodingCompany/filament-socialite/pull/76 ## [1.2.0 - 2024-01-31](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.1.1...1.2.0) - Add option to add optional parameters in https://github.com/DutchCodingCompany/filament-socialite/pull/69 ## [1.1.1 - 2024-01-18](https://github.com/DutchCodingCompany/filament-socialite/compare/1.1.0...1.1.1) - Improve domain routing in https://github.com/DutchCodingCompany/filament-socialite/pull/61 - Update README in https://github.com/DutchCodingCompany/filament-socialite/pull/64 ## [1.1.0 - 2024-01-08](https://github.com/DutchCodingCompany/filament-socialite/compare/1.0.1...1.1.0) - Add button customization options in https://github.com/DutchCodingCompany/filament-socialite/pull/59 ## [1.0.1 - 2023-12-18](https://github.com/DutchCodingCompany/filament-socialite/compare/1.0.0...1.0.1) - Resolve plugin registration issue [#54](https://github.com/DutchCodingCompany/filament-socialite/issues/54) ## [1.0.0 - 2023-12-05](https://github.com/DutchCodingCompany/filament-socialite/compare/0.2.2...1.0.0) - Added support for Filament v3 through the plugin setup - Added support for multiple panels - See [UPGRADE.md](UPGRADE.md) ## 0.2.2 - 2022-06-14 ### What's Changed - Fix readme by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/15 - use Filament-fortify render hook by @wychoong in https://github.com/DutchCodingCompany/filament-socialite/pull/16 ### New Contributors - @wychoong made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/16 **Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.2.1...0.2.2 ## 0.2.1 - 2022-05-25 ## What's Changed - Fix user model instantiating by @marcoboers in https://github.com/DutchCodingCompany/filament-socialite/pull/14 **Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.2.0...0.2.1 ## 0.2.0 - 2022-05-24 ## Breaking changes - `Events\DomainFailed` renamed to `Events\UserNotAllowed` - `Events\RegistrationFailed` renamed to `Events\RegistrationNotEnabled` ## What's Changed - Refactor the controller for extendability and customization by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/13 ## New Contributors - @dododedodonl made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/13 **Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.5...0.2.0 ## 0.1.5 - 2022-05-20 ## What's Changed - Fix missing variable for registered event by @marcoboers in https://github.com/DutchCodingCompany/filament-socialite/pull/11 **Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.4...0.1.5 ## 0.1.4 - 2022-05-06 ## What's Changed - Feature: Adds buttons blade component by @oyepez003 in https://github.com/DutchCodingCompany/filament-socialite/pull/8 - Feature: Add login events dispatching by @marcoboers in https://github.com/DutchCodingCompany/filament-socialite/pull/5 **Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.3...0.1.4 ## 0.1.3 - 2022-05-04 ## What's Changed - Bugfix: Avoid returning 403 when a user exists based on the oauth-email . by @oyepez003 in https://github.com/DutchCodingCompany/filament-socialite/pull/7 ## New Contributors - @oyepez003 made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/7 **Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.2...0.1.3 ## 0.1.2 - 2022-05-03 ## What's Changed - Bump dependabot/fetch-metadata from 1.3.0 to 1.3.1 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/2 - Add Laravel 8 support and make fontawesome icons optional by @marcoboers in https://github.com/DutchCodingCompany/filament-socialite/pull/4 ## New Contributors - @marcoboers made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/4 **Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.1...0.1.2 ## 0.1.1 - 2022-04-11 ## What's Changed - Fix registration flow ## 0.1.0 - 2022-04-08 ### Initial Release - Add social login links to login page - Support Socialite OAuth flow - Support registration flow - Support domain allowlist for internal use - Dark mode support - Blade Font Awesome brand icons ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) DutchCodingCompany Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Social login for Filament through Laravel Socialite [![Latest Version on Packagist](https://img.shields.io/packagist/v/dutchcodingcompany/filament-socialite.svg?style=flat-square)](https://packagist.org/packages/dutchcodingcompany/filament-socialite) [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/dutchcodingcompany/filament-socialite/run-tests?label=tests)](https://github.com/dutchcodingcompany/filament-socialite/actions?query=workflow%3Arun-tests+branch%3Amain) [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/dutchcodingcompany/filament-socialite/Check%20&%20fix%20styling?label=code%20style)](https://github.com/dutchcodingcompany/filament-socialite/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/dutchcodingcompany/filament-socialite.svg?style=flat-square)](https://packagist.org/packages/dutchcodingcompany/filament-socialite) Add OAuth2 login through Laravel Socialite to Filament. OAuth1 (eg. Twitter) is not supported at this time. ## Installation | Filament version | Package version | Readme | |----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|--------------------------------------------------------------------------------------| | [^5.0.0](https://github.com/filamentphp/filament/tree/5.x) | ^3.1 | [Link](https://github.com/DutchCodingCompany/filament-socialite/blob/main/README.md) | | [^4.0.0](https://github.com/filamentphp/filament/tree/4.x) | 3.x.x | [Link](https://github.com/DutchCodingCompany/filament-socialite/blob/main/README.md) | | [^3.2.44](https://github.com/filamentphp/filament/releases/tag/v3.2.44) (if using [SPA mode](https://filamentphp.com/docs/3.x/panels/configuration#spa-mode)) | 2.x.x | [Link](https://github.com/DutchCodingCompany/filament-socialite/blob/2.x/README.md) | | [^3.2.44](https://github.com/filamentphp/filament/releases/tag/v3.2.44) (if using [SPA mode](https://filamentphp.com/docs/3.x/panels/configuration#spa-mode)) | ^1.3.1 | | | 3.x | 1.x.x | [Link](https://github.com/DutchCodingCompany/filament-socialite/blob/1.x/README.md) | | 2.x | 0.x.x | | Install the package via composer: ```bash composer require dutchcodingcompany/filament-socialite ``` Publish and migrate the migration file: ```bash php artisan vendor:publish --tag="filament-socialite-migrations" php artisan migrate ``` Other configuration files include: ```bash php artisan vendor:publish --tag="filament-socialite-config" php artisan vendor:publish --tag="filament-socialite-views" php artisan vendor:publish --tag="filament-socialite-translations" ``` You need to register the plugin in the Filament panel provider (the default filename is `app/Providers/Filament/AdminPanelProvider.php`). The following options are available: ```php use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use DutchCodingCompany\FilamentSocialite\Provider; use Filament\Support\Colors; use Laravel\Socialite\Contracts\User as SocialiteUserContract; use Illuminate\Contracts\Auth\Authenticatable; // ... ->plugin( FilamentSocialitePlugin::make() // (required) Add providers corresponding with providers in `config/services.php`. ->providers([ // Create a provider 'gitlab' corresponding to the Socialite driver with the same name. Provider::make('gitlab') ->label('GitLab') ->icon('fab-gitlab') ->color(Color::hex('#2f2a6b')) ->outlined(false) ->stateless(false) ->scopes(['...']) ->with(['...']), ]) // (optional) Override the panel slug to be used in the oauth routes. Defaults to the panel's configured path. ->slug('admin') // (optional) Enable/disable registration of new (socialite-) users. ->registration(true) // (optional) Enable/disable registration of new (socialite-) users using a callback. // In this example, a login flow can only continue if there exists a user (Authenticatable) already. ->registration(fn (string $provider, SocialiteUserContract $oauthUser, ?Authenticatable $user) => (bool) $user) // (optional) Change the associated model class. ->userModelClass(\App\Models\User::class) // (optional) Change the associated socialite class (see below). ->socialiteUserModelClass(\App\Models\SocialiteUser::class) ); ``` This package automatically adds 2 routes per panel to make the OAuth flow possible: a redirector and a callback. When setting up your **external OAuth app configuration**, enter the following callback URL (in this case for the Filament panel with ID `admin` and the `github` provider): ``` https://example.com/admin/oauth/callback/github ``` A multi-panel callback route is available as well that does not contain the panel ID in the url. Instead, it determines the panel ID from an encrypted `state` input (`...?state=abcd1234`). This allows you to create a single OAuth application for multiple Filament panels that use the same callback URL. Note that this only works for _stateful_ OAuth apps: ``` https://example.com/oauth/callback/github ``` If in doubt, run `php artisan route:list` to see which routes are available to you. ### Icons You can specify a custom icon for each of your login providers. You can add Font Awesome brand icons made available through [Blade Font Awesome](https://github.com/owenvoke/blade-fontawesome) by running: ```bash composer require owenvoke/blade-fontawesome ``` ### Registration flow This package supports account creation for users. However, to support this flow it is important that the `password` attribute on your `User` model is nullable. For example, by adding the following to your users table migration. Or you could opt for customizing the user creation, see below. ```php $table->string('password')->nullable(); ``` ### Domain Allow list This package supports the option to limit the users that can login with the OAuth login to users of a certain domain. This can be used to setup SSO for internal use. ```php ->plugin( FilamentSocialitePlugin::make() // ... ->registration(true) ->domainAllowList(['localhost']) ); ``` ### Changing how an Authenticatable user is created or retrieved You can use the `createUserUsing` and `resolveUserUsing` methods to change how a user is created or retrieved. ```php use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use Laravel\Socialite\Contracts\User as SocialiteUserContract; ->plugin( FilamentSocialitePlugin::make() // ... ->createUserUsing(function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) { // Logic to create a new user. }) ->resolveUserUsing(function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) { // Logic to retrieve an existing user. }) ... ); ``` ### Change how a Socialite user is created or retrieved In your plugin options in your Filament panel, add the following method: ```php // app/Providers/Filament/AdminPanelProvider.php ->plugins([ FilamentSocialitePlugin::make() // ... ->socialiteUserModelClass(\App\Models\SocialiteUser::class) ``` This class should at the minimum implement the [`FilamentSocialiteUser`](/src/Models/Contracts/FilamentSocialiteUser.php) interface, like so: ```php namespace App\Models; use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract; use Illuminate\Contracts\Auth\Authenticatable; use Laravel\Socialite\Contracts\User as SocialiteUserContract; class SocialiteUser implements FilamentSocialiteUserContract { public function getUser(): Authenticatable { // } public static function findForProvider(string $provider, SocialiteUserContract $oauthUser): ?self { // } public static function createForProvider( string $provider, SocialiteUserContract $oauthUser, Authenticatable $user ): self { // } } ``` ### Check if the user is authorized to use the application You can use the `authorizeUserUsing` method to check if the user is authorized to use the application. **Note:** by [default](/src/Traits/Callbacks.php#L145) this method check if the user's email domain is in the domain allow list. ```php use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use Laravel\Socialite\Contracts\User as SocialiteUserContract; ->plugin( FilamentSocialitePlugin::make() // ... ->authorizeUserUsing(function (FilamentSocialitePlugin $plugin, SocialiteUserContract $oauthUser) { // Logic to authorize the user. return FilamentSocialitePlugin::checkDomainAllowList($plugin, $oauthUser); }) // ... ); ``` ### Change login redirect When your panel has [multi-tenancy](https://filamentphp.com/docs/4.x/users/tenancy) enabled, after logging in, the user will be redirected to their [default tenant](https://filamentphp.com/docs/4.x/users/tenancy#setting-the-default-tenant). If you want to change this behavior, you can call the 'redirectAfterLoginUsing' method on the `FilamentSocialitePlugin`. ```php use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract; use DutchCodingCompany\FilamentSocialite\Models\SocialiteUser; FilamentSocialitePlugin::make() ->redirectAfterLoginUsing(function (string $provider, FilamentSocialiteUserContract $socialiteUser, FilamentSocialitePlugin $plugin) { // Change the redirect behaviour here. }); ``` ## Events There are a few events dispatched during the authentication process: * `InvalidState(InvalidStateException $exception)`: When trying to retrieve the oauth (socialite) user, an invalid state was encountered * `Login(FilamentSocialiteUserContract $socialiteUser)`: When a user successfully logs in * `Registered(string $provider, SocialiteUserContract $oauthUser, FilamentSocialiteUserContract $socialiteUser)`: When a user and socialite user is successfully registered and logged in (when enabled in config) * `RegistrationNotEnabled(string $provider, SocialiteUserContract $oauthUser, ?Auhthenticatable $user)`: When a user tries to login with an unknown account and registration is not enabled * `SocialiteUserConnected(string $provider, SocialiteUserContract $oauthUser, FilamentSocialiteUserContract $socialiteUser)`: When a socialite user is created for an existing user * `UserNotAllowed(SocialiteUserContract $oauthUser)`: When a user tries to login with an email which domain is not on the allowlist ## Scopes Scopes can be added to the provider on the panel, for example: ```php use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use DutchCodingCompany\FilamentSocialite\Provider; FilamentSocialitePlugin::make() ->providers([ Provider::make('github') ->label('Github') ->icon('fab-github') ->scopes([ // Add scopes here. 'read:user', 'public_repo', ]), ]), ``` ## Optional parameters You can add [optional parameters](https://laravel.com/docs/10.x/socialite#optional-parameters) to the request by adding a `with` key to the provider on the panel, for example: ```php use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use DutchCodingCompany\FilamentSocialite\Provider; FilamentSocialitePlugin::make() ->providers([ Provider::make('github') ->label('Github') ->icon('fab-github') ->with([ // Add scopes here. // Add optional parameters here. 'hd' => 'example.com', ]), ]), ``` ## Visibility You can set the visibility of a provider, if it is not visible, buttons will not be rendered. All functionality will still be enabled. ```php use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use DutchCodingCompany\FilamentSocialite\Provider; FilamentSocialitePlugin::make() ->providers([ Provider::make('github') ->visible(fn () => true), ]), ``` ## Stateless Authentication You can add `stateless` parameters to the provider configuration in the config/services.php config file, for example: ```php 'apple' => [ 'client_id' => '...', 'client_secret' => '...', 'stateless'=>true, ] ``` **Note:** you cannot use the `state` parameter, as it is used to determine from which Filament panel the user came from. ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security Vulnerabilities Please review [our security policy](../../security/policy) on how to report security vulnerabilities. ## Credits - [All Contributors](../../contributors) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Use this section to tell people about which versions of your project are currently being supported with security updates. | Version | Supported | | ------- | ------------------ | | 3.x | :white_check_mark: | | 2.x | :warning: (security fixes only) | | 1.x | :no_entry_sign: | | 0.x | :no_entry_sign: | ## Reporting a Vulnerability If you discover a security vulnerability within this plugin, please email Dutch Coding Company via [server@dutchcodingcompany.com](mailto:server@dutchcodingcompany.com). All security vulnerabilities will be promptly addressed. ================================================ FILE: UPGRADE.md ================================================ # Upgrade Guide ## `2.x.x` to `3.x.x` (Filament v4.x) ### Slug In v3 of the plugin, the panel's configured path is used instead of it's ID when generating the callback URLs. In order to revert to previous behaviour, use slug to override the behaviour: ```php ->plugin( FilamentSocialitePlugin::make() ->slug('admin') // change this to the panel's ID // other config for plugin ) ``` ## `1.x.x` to `2.x.x` (Filament v3.x) For version 2 we refactored most of the plugin to be more consistent with the Filament naming conventions. We've also moved some of the callbacks to the plugin, so they are configurable per panel. ### Method names Every method name has been changed to be more consistent with the Filament naming conventions. The following changes have been made: - `setProviders()` -> `providers()` - `setSlug()` -> `slug()` - `setLoginRouteName()` -> `loginRouteName()` - `setDashboardRouteName()` -> `dashboardRouteName()` - `setRememberLogin()` -> `rememberLogin()` - `setRegistrationEnabled()` -> `registration()` - `getRegistrationEnabled()` -> `getRegistration()` - `setDomainAllowList()` -> `domainAllowList()` - `setSocialiteUserModelClass()` -> `socialiteUserModelClass()` - `setUserModelClass()` -> `userModelClass()` - `setShowDivider()` -> `showDivider()` **Note:** We've included a simple rector script which automatically updates the method names. It checks all panel providers in the `app\Provider\Filament` directory. You can run the script by executing the following command: ```bash vendor/bin/upgrade-v2 ``` #### Callbacks **setCreateUserCallback()** The `setCreateUserCallback()` has been renamed to `createUserUsing()`. This function was first registered in the `boot` method of your `AppServiceProvider.php`, but now it should be called on the plugin. ```php FilamentSocialitePlugin::make() // ... ->createUserUsing(function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) { // Logic to create a new user. }) ``` **setUserResolver()** The `setUserResolver()` has been renamed to `resolveUserUsing()`. This function was first registered in the `boot` method of your `AppServiceProvider.php`, but now it should be called on the plugin. ```php FilamentSocialitePlugin::make() // ... ->resolveUserUsing(function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) { // Logic to retrieve an existing user. }) ``` **setLoginRedirectCallback()** The `setLoginRedirectCallback()` has been renamed to `redirectAfterLoginUsing()`. This function was first registered in the `boot` method of your `AppServiceProvider.php`, but now it should be called on the plugin. ```php FilamentSocialitePlugin::make() // ... ->redirectAfterLoginUsing(function (string $provider, FilamentSocialiteUserContract $socialiteUser, FilamentSocialitePlugin $plugin) { // Change the redirect behaviour here. }) ``` #### Removals **getOptionalParameters()** This function was used internally only inside the `SocialiteLoginController`. If you haven't extended this controller, you can ignore this change. Provider details can now be retrieved using `$plugin->getProvider($provider)->getWith()`. **getProviderScopes()** This function was used internally only inside the `SocialiteLoginController`. If you haven't extended this controller, you can ignore this change. Provider details can now be retrieved using `$plugin->getProvider($provider)->getScopes()`. ### Configuration **Providers** Previously, providers were configured by passing a plain array. In the new setup, they should be created using the `Provider` class. The key should be passed as part of the `make()` function. ```php use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use DutchCodingCompany\FilamentSocialite\Provider; FilamentSocialitePlugin::make() ->providers([ Provider::make('gitlab') ->label('GitLab') ->icon('fab-gitlab') ->color(Color::hex('#2f2a6b')), ]), ``` **Scopes and Optional parameters** Scopes and additional parameters for Socialite providers were previously configured in the `services.php` file, but have now been moved to the `->providers()` method on the Filament plugin. ```php use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use DutchCodingCompany\FilamentSocialite\Provider; FilamentSocialitePlugin::make() ->providers([ Provider::make('gitlab') // ... ->scopes([ // Add scopes here. 'read:user', 'public_repo', ]), ->with([ // Add optional parameters here. 'hd' => 'example.com', ]), ]), ``` ## `0.x.x` to `1.x.x` (Filament v3.x) - Replace/republish the configuration file: - `sail artisan vendor:publish --provider="DutchCodingCompany\FilamentSocialite\FilamentSocialiteServiceProvider"` - Update your panel configuration `App\Providers\Filament\YourPanelProvider` to include the plugin: - Append `->plugins([\DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin::make()])` - Configure any options by chaining functions on the plugin. ## `0.x.x` (Filament v2.x) - Initial version ================================================ FILE: bin/upgrade-v2 ================================================ #!/usr/bin/env php [ \Illuminate\Cookie\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, ], ]; ================================================ FILE: database/migrations/create_socialite_users_table.php.stub ================================================ id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate(); $table->string('provider'); $table->string('provider_id'); $table->timestamps(); $table->unique([ 'provider', 'provider_id', ]); }); } public function down(): void { Schema::dropIfExists('socialite_users'); } }; ================================================ FILE: package.json ================================================ { "private": true, "name": "filament-socialite", "devDependencies": { "@tailwindcss/cli": "^4.1.10", "@tailwindcss/postcss": "^4.1.10", "tailwindcss": "^4.1.10" }, "scripts": { "dev": "npx @tailwindcss/cli -i ./resources/css/plugin.css -o ./resources/dist/plugin.css --watch", "watch": "npx @tailwindcss/cli -i ./resources/css/plugin.css -o ./resources/dist/plugin.css --watch", "prod": "npx @tailwindcss/cli -i ./resources/css/plugin.css -o ./resources/dist/plugin.css --minify" } } ================================================ FILE: phpstan-baseline.neon ================================================ parameters: ignoreErrors: - message: '#^Class App\\Models\\User not found\.$#' identifier: class.notFound count: 1 path: src/FilamentSocialitePlugin.php - message: '#^Property DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin\:\:\$userModelClass \(class\-string\\) does not accept default value of type string\.$#' identifier: property.defaultValue count: 1 path: src/FilamentSocialitePlugin.php - message: '#^Call to function method_exists\(\) with ''Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\VerifyCsrfToken'' and ''except'' will always evaluate to true\.$#' identifier: function.alreadyNarrowedType count: 1 path: src/FilamentSocialiteServiceProvider.php - message: '#^Parameter \#1 \$value of method DutchCodingCompany\\FilamentSocialite\\Http\\Controllers\\SocialiteLoginController\:\:evaluate\(\) expects bool\|\(callable\(\)\: bool\), bool\|\(Closure\(string, Laravel\\Socialite\\Contracts\\User, Illuminate\\Contracts\\Auth\\Authenticatable\|null\)\: bool\) given\.$#' identifier: argument.type count: 1 path: src/Http/Controllers/SocialiteLoginController.php - message: '#^Method DutchCodingCompany\\FilamentSocialite\\Models\\SocialiteUser\:\:user\(\) should return Illuminate\\Database\\Eloquent\\Relations\\BelongsTo\ but returns Illuminate\\Database\\Eloquent\\Relations\\BelongsTo\\.$#' identifier: return.type count: 1 path: src/Models/SocialiteUser.php - message: '#^Parameter \#1 \$view of function view expects view\-string\|null, string given\.$#' identifier: argument.type count: 1 path: src/View/Components/Buttons.php - message: '#^PHPDoc tag @property for property DutchCodingCompany\\FilamentSocialite\\Tests\\Fixtures\\TestTenantUser\:\:\$teams contains generic class Illuminate\\Support\\Collection but does not specify its types\: TKey, TValue$#' identifier: missingType.generics count: 1 path: tests/Fixtures/TestTenantUser.php - message: '#^Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage\:\:andReturn\(\)\.$#' identifier: method.notFound count: 3 path: tests/TestCase.php - message: '#^Parameter \#1 \$callback of static method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:guessFactoryNamesUsing\(\) expects callable\(class\-string\\)\: class\-string\, Closure\(string\)\: non\-falsy\-string given\.$#' identifier: argument.type count: 1 path: tests/TestCase.php ================================================ FILE: phpstan.neon.dist ================================================ includes: - ./vendor/larastan/larastan/extension.neon - ./phpstan-baseline.neon parameters: level: 8 paths: - config - database - src - tests ================================================ FILE: phpunit.xml ================================================ tests src ================================================ FILE: rector.php ================================================ paths([ 'app/Providers/Filament/', ]); $rectorConfig->ruleWithConfiguration( \Rector\Renaming\Rector\MethodCall\RenameMethodRector::class, [ new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setProviders", "providers", ), new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setRegistrationEnabled", "registration", ), new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setSlug", "slug", ), new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setLoginRouteName", "loginRouteName", ), new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setDashboardRouteName", "dashboardRouteName", ), new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setRememberLogin", "rememberLogin", ), new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setSocialiteUserModelClass", "socialiteUserModelClass", ), new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setDomainAllowList", "domainAllowList", ), new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setUserModelClass", "userModelClass", ), new \Rector\Renaming\ValueObject\MethodCallRename( "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin", "setShowDivider", "showDivider", ), ] ); }; ================================================ FILE: resources/css/plugin.css ================================================ @import "tailwindcss"; @config '../../tailwind.config.js'; ================================================ FILE: resources/dist/plugin.css ================================================ /*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-800:oklch(27.8% .033 256.848);--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--font-weight-medium:500;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.static{position:static}.contents{display:contents}.flex{display:flex}.grid{display:grid}.inline-block{display:inline-block}.table{display:table}.h-px{height:1px}.w-full{width:100%}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:calc(var(--spacing)*4)}.gap-y-6{row-gap:calc(var(--spacing)*6)}.rounded-full{border-radius:3.40282e38px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.bg-white{background-color:var(--color-white)}.p-2{padding:calc(var(--spacing)*2)}.text-center{text-align:center}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.text-gray-500{color:var(--color-gray-500)}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark\:text-gray-100{color:var(--color-gray-100)}}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false} ================================================ FILE: resources/lang/en/auth.php ================================================ 'Or log in via', 'login-failed' => 'Login failed, please try again.', 'user-not-allowed' => 'Your email is not part of a domain that is allowed.', 'registration-not-enabled' => 'Registration of a new user is not allowed.', ]; ================================================ FILE: resources/views/.gitkeep ================================================ ================================================ FILE: resources/views/components/buttons.blade.php ================================================
@if ($messageBag->isNotEmpty()) @foreach($messageBag->all() as $value)

{{ __($value) }}

@endforeach @endif @if (count($visibleProviders)) @if($showDivider)

{{ __('filament-socialite::auth.login-via') }}

@endif
@foreach($visibleProviders as $key => $provider) {{ $provider->getLabel() }} @endforeach
@else @endif
================================================ FILE: routes/web.php ================================================ hasPlugin('filament-socialite')) { continue; } // Retrieve slug for route name. $slug = $panel->getPlugin('filament-socialite')->getSlug(); $domains = $panel->getDomains(); foreach ((empty($domains) ? [null] : $domains) as $domain) { Filament::currentDomain($domain); Route::domain($domain) ->middleware($panel->getMiddleware()) ->name("socialite.{$panel->generateRouteName('oauth.redirect')}") ->get("/$slug/oauth/{provider}", [SocialiteLoginController::class, 'redirectToProvider']); Route::domain($domain) ->match(['get', 'post'], "$slug/oauth/callback/{provider}", [SocialiteLoginController::class, 'processCallback']) ->middleware([ ...$panel->getMiddleware(), ...config('filament-socialite.middleware'), ]) ->name("socialite.{$panel->generateRouteName('oauth.callback')}"); Filament::currentDomain(null); } } /** * @note This route can only distinguish between Filament panels using the `state` input. If you have a stateless OAuth * implementation, use the "$slug/oauth/callback/{provider}" route instead which has the panel in the URL itself. */ Route::match(['get', 'post'], "/oauth/callback/{provider}", [SocialiteLoginController::class, 'processCallback']) ->middleware([ PanelFromUrlQuery::class, ...config('filament-socialite.middleware'), ]) ->name('oauth.callback'); ================================================ FILE: src/Events/InvalidState.php ================================================ */ protected array $providers = []; /** * @var array */ protected array $domainAllowList = []; protected bool $rememberLogin = false; /** * @phpstan-var (\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, ?\Illuminate\Contracts\Auth\Authenticatable $user): bool) | bool */ protected Closure | bool $registration = false; protected ?string $slug = null; protected ?string $panelId = null; protected bool $showDivider = true; public function __construct( protected Repository $config, protected Factory $auth, ) { // } public static function make(): static { return app(static::class); } public static function current(): static { if (Filament::getCurrentPanel()?->hasPlugin('filament-socialite')) { /** @var static $plugin */ $plugin = Filament::getCurrentPanel()->getPlugin('filament-socialite'); return $plugin; } throw new ImplementationException('No current panel found with filament-socialite plugin.'); } public function getId(): string { return 'filament-socialite'; } public function register(Panel $panel): void { $this->panelId = $panel->getId(); } public function boot(Panel $panel): void { // } /** * @param array $providers */ public function providers(array $providers): static { // Assign providers as key-value pairs with the provider name as the key. $this->providers = Arr::mapWithKeys( $providers, static fn (Provider $value) => [$value->getName() => $value], ); return $this; } /** * @return array */ public function getProviders(): array { return $this->providers; } public function getProvider(string $provider): Provider { if (! $this->isProviderConfigured($provider)) { throw ProviderNotConfigured::make($provider); } return $this->providers[$provider]; } public function slug(?string $slug): static { $this->slug = $slug; return $this; } public function getSlug(): string { return $this->slug ?? rtrim($this->getPanel()->getPath(), '/'); } public function rememberLogin(bool $value): static { $this->rememberLogin = $value; return $this; } public function getRememberLogin(): bool { return $this->rememberLogin; } /** * @param (\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, ?\Illuminate\Contracts\Auth\Authenticatable $user): bool) | bool $value * @return $this */ public function registration(Closure | bool $value = true): static { $this->registration = $value; return $this; } /** * @return (\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, ?\Illuminate\Contracts\Auth\Authenticatable $user): bool) | bool */ public function getRegistration(): Closure | bool { return $this->registration; } /** * @param array $values */ public function domainAllowList(array $values): static { $this->domainAllowList = $values; return $this; } /** * @return array */ public function getDomainAllowList(): array { return $this->domainAllowList; } public function isProviderConfigured(string $provider): bool { return $this->config->has('services.'.$provider) && isset($this->providers[$provider]); } public function showDivider(bool $divider): static { $this->showDivider = $divider; return $this; } public function getShowDivider(): bool { return $this->showDivider; } public function getPanel(): Panel { return Filament::getPanel($this->getPanelId()); } public function getPanelId(): string { return $this->panelId ?? throw new ImplementationException('Panel ID not set.'); } public function getGuard(): StatefulGuard { $guard = $this->auth->guard( $guardName = $this->getPanel()->getAuthGuard() ); if ($guard instanceof StatefulGuard) { return $guard; } throw GuardNotStateful::make($guardName); } } ================================================ FILE: src/FilamentSocialiteServiceProvider.php ================================================ name('filament-socialite') ->hasConfigFile() ->hasTranslations() ->hasViews() ->hasRoute('web') ->hasMigration('create_socialite_users_table'); } public function packageRegistered(): void { // } public function packageBooted(): void { Blade::componentNamespace('DutchCodingCompany\FilamentSocialite\View\Components', 'filament-socialite'); Blade::component('buttons', Buttons::class); FilamentAsset::register([ Css::make('filament-socialite-styles', __DIR__.'/../resources/dist/plugin.css')->loadedOnRequest(), ], package: 'filament-socialite'); FilamentView::registerRenderHook( 'panels::auth.login.form.after', static function (): ?string { $panel = Filament::getCurrentPanel(); if (! $panel?->hasPlugin('filament-socialite')) { return null; } /** @var \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin */ $plugin = $panel->getPlugin('filament-socialite'); return Blade::render(''); }, ); if ( version_compare(app()->version(), '11.0', '>=') && method_exists(VerifyCsrfToken::class, 'except') ) { VerifyCsrfToken::except([ '*/oauth/callback/*', 'oauth/callback/*', ]); } } } ================================================ FILE: src/Http/Controllers/SocialiteLoginController.php ================================================ plugin()->isProviderConfigured($provider)) { throw ProviderNotConfigured::make($provider); } /** @var \Laravel\Socialite\Two\AbstractProvider $driver */ $driver = Socialite::driver($provider); $response = $driver ->with([ ...$this->plugin()->getProvider($provider)->getWith(), 'state' => $state = PanelFromUrlQuery::encrypt($this->plugin()->getPanel()->getId()), ]) ->scopes($this->plugin()->getProvider($provider)->getScopes()) ->redirect(); // Set state value to be equal to the encrypted panel id. This value is used to // retrieve the panel id once the authentication returns to our application, // and it still prevents CSRF as it is non-guessable value. session()->put('state', $state); return $response; } protected function retrieveOauthUser(string $provider): ?SocialiteUserContract { $stateless = $this->plugin()->getProvider($provider)->getStateless(); try { /** @var \Laravel\Socialite\Two\AbstractProvider $driver */ $driver = Socialite::driver($provider); return $stateless ? $driver->stateless()->user() : $driver->user(); } catch (InvalidStateException $e) { Events\InvalidState::dispatch($e); } return null; } protected function retrieveSocialiteUser(string $provider, SocialiteUserContract $oauthUser): ?FilamentSocialiteUserContract { return $this->plugin()->getSocialiteUserModel()::findForProvider($provider, $oauthUser); } protected function redirectToLogin(string $message): RedirectResponse { // Add error message to the session, this way we can show an error message on the form. session()->flash('filament-socialite-login-error', __($message)); return redirect()->route($this->plugin()->getLoginRouteName()); } protected function authorizeUser(SocialiteUserContract $oauthUser): bool { return app()->call($this->plugin()->getAuthorizeUserUsing(), ['plugin' => $this->plugin(), 'oauthUser' => $oauthUser]); } protected function loginUser(string $provider, FilamentSocialiteUserContract $socialiteUser, SocialiteUserContract $oauthUser): Response { // Log the user in $this->plugin()->getGuard()->login($socialiteUser->getUser(), $this->plugin()->getRememberLogin()); // Dispatch the login event Events\Login::dispatch($socialiteUser, $oauthUser); return app()->call($this->plugin()->getRedirectAfterLoginUsing(), ['provider' => $provider, 'socialiteUser' => $socialiteUser, 'plugin' => $this->plugin]); } protected function registerSocialiteUser(string $provider, SocialiteUserContract $oauthUser, Authenticatable $user): Response { // Create a socialite user $socialiteUser = $this->plugin()->getSocialiteUserModel()::createForProvider($provider, $oauthUser, $user); // Dispatch the socialite user connected event Events\SocialiteUserConnected::dispatch($provider, $oauthUser, $socialiteUser); // Login the user return $this->loginUser($provider, $socialiteUser, $oauthUser); } protected function registerOauthUser(string $provider, SocialiteUserContract $oauthUser): Response { $socialiteUser = DB::transaction(function () use ($provider, $oauthUser) { // Create a user $user = app()->call($this->plugin()->getCreateUserUsing(), ['provider' => $provider, 'oauthUser' => $oauthUser, 'plugin' => $this->plugin]); // Create a socialite user return $this->plugin()->getSocialiteUserModel()::createForProvider($provider, $oauthUser, $user); }); // Dispatch the registered event Events\Registered::dispatch($provider, $oauthUser, $socialiteUser); // Login the user return $this->loginUser($provider, $socialiteUser, $oauthUser); } public function processCallback(string $provider): Response { if (! $this->plugin()->isProviderConfigured($provider)) { throw ProviderNotConfigured::make($provider); } // Try to retrieve existing user $oauthUser = $this->retrieveOauthUser($provider); if (is_null($oauthUser)) { return $this->redirectToLogin('filament-socialite::auth.login-failed'); } // Verify if the user is authorized. if (! $this->authorizeUser($oauthUser)) { Events\UserNotAllowed::dispatch($oauthUser); return $this->redirectToLogin('filament-socialite::auth.user-not-allowed'); } // Try to find a socialite user $socialiteUser = $this->retrieveSocialiteUser($provider, $oauthUser); if ($socialiteUser) { return $this->loginUser($provider, $socialiteUser, $oauthUser); } // See if a user already exists, but not for this socialite provider $user = app()->call($this->plugin()->getResolveUserUsing(), [ 'provider' => $provider, 'oauthUser' => $oauthUser, 'plugin' => $this->plugin, ]); // See if registration is allowed if (! $this->evaluate($this->plugin()->getRegistration(), ['provider' => $provider, 'oauthUser' => $oauthUser, 'user' => $user])) { Events\RegistrationNotEnabled::dispatch($provider, $oauthUser, $user); return $this->redirectToLogin('filament-socialite::auth.registration-not-enabled'); } // Handle registration return $user ? $this->registerSocialiteUser($provider, $oauthUser, $user) : $this->registerOauthUser($provider, $oauthUser); } protected function plugin(): FilamentSocialitePlugin { return $this->plugin ??= FilamentSocialitePlugin::current(); } } ================================================ FILE: src/Http/Middleware/PanelFromUrlQuery.php ================================================ handle($request, $next, static::decrypt($request)); } public static function encrypt(string $panel): string { return Crypt::encrypt($panel); } /** * @throws InvalidCallbackPayload */ public static function decrypt(Request $request): string { try { if (! is_string($request->input('state'))) { throw new DecryptException('State is not a string.'); } return Crypt::decrypt($request->input('state')); } catch (DecryptException $e) { throw InvalidCallbackPayload::make($e); } } } ================================================ FILE: src/Models/Contracts/FilamentSocialiteUser.php ================================================ */ public function user(): BelongsTo { /** @var class-string<\Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable> */ $user = FilamentSocialitePlugin::current()->getUserModelClass(); return $this->belongsTo($user); } public function getUser(): Authenticatable { assert($this->user instanceof Authenticatable); return $this->user; } public static function findForProvider(string $provider, SocialiteUserContract $oauthUser): ?self { return self::query() ->where('provider', $provider) ->where('provider_id', $oauthUser->getId()) ->first(); } public static function createForProvider(string $provider, SocialiteUserContract $oauthUser, Authenticatable $user): self { return self::query() ->create([ 'user_id' => $user->getKey(), 'provider' => $provider, 'provider_id' => $oauthUser->getId(), ]); } } ================================================ FILE: src/Provider.php ================================================ | array */ protected Closure | array $scopes = []; /** * @var \Closure(): array | array */ protected Closure | array $with = []; protected bool $stateless = false; public function __construct(string $name) { $this->name($name); } public static function make(string $name): static { return app(static::class, ['name' => $name]); } /** * @param array $attributes */ public function fill(array $attributes): static { foreach ($attributes as $key => $value) { $this->{$key}($value); } return $this; } public function name(string $name): static { $this->name = $name; return $this; } public function getName(): string { return $this->name; } public function label(string $label): static { $this->label = $label; return $this; } public function getLabel(): string { return $this->label ?? Str::title($this->getName()); } public function icon(string | null $icon): static { $this->icon = $icon; return $this; } public function getIcon(): string | null { return $this->icon; } /** * @param string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null $color */ public function color(string | array | null $color): static { $this->color = $color; return $this; } /** * @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null */ public function getColor(): string | array | null { return $this->color; } public function outlined(bool $outlined = true): static { $this->outlined = $outlined; return $this; } public function getOutlined(): bool { return $this->outlined; } /** * @param Closure(): array | array $scopes */ public function scopes(Closure | array $scopes): static { $this->scopes = $scopes; return $this; } /** * @return array */ public function getScopes(): array { return $this->evaluate($this->scopes, ['provider' => $this]); } /** * @param Closure(): array | array $with */ public function with(Closure | array $with): static { $this->with = $with; return $this; } /** * @return array */ public function getWith(): array { return $this->evaluate($this->with, ['provider' => $this]); } public function stateless(bool $stateless = true): static { $this->stateless = $stateless; return $this; } public function getStateless(): bool { return $this->stateless; } } ================================================ FILE: src/Traits/Callbacks.php ================================================ createUserUsing = $callback; return $this; } /** * @return \Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): \Illuminate\Contracts\Auth\Authenticatable */ public function getCreateUserUsing(): Closure { return $this->createUserUsing ?? function ( string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin, ) { /** * @var \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable> $query */ $query = (new $this->userModelClass())->query(); return $query->create([ 'name' => $oauthUser->getName(), 'email' => $oauthUser->getEmail(), ]); }; } /** * @param \Closure(string $provider, \DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser $socialiteUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): \Illuminate\Http\RedirectResponse $callback */ public function redirectAfterLoginUsing(Closure $callback): static { $this->redirectAfterLoginUsing = $callback; return $this; } /** * @return \Closure(string $provider, \DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser $socialiteUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): \Symfony\Component\HttpFoundation\Response */ public function getRedirectAfterLoginUsing(): Closure { return $this->redirectAfterLoginUsing ?? function (string $provider, FilamentSocialiteUserContract $socialiteUser, FilamentSocialitePlugin $plugin) { if (($panel = $this->getPanel())->hasTenancy()) { $tenant = Filament::getUserDefaultTenant($socialiteUser->getUser()); if (is_null($tenant) && $tenantRegistrationUrl = $panel->getTenantRegistrationUrl()) { return redirect()->intended($tenantRegistrationUrl); } return redirect()->intended( $panel->getUrl($tenant) ); } return redirect()->intended( $this->getPanel()->getUrl() ); }; } /** * @param ?\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): ?(\Illuminate\Contracts\Auth\Authenticatable) $callback */ public function resolveUserUsing(?Closure $callback = null): static { $this->resolveUserUsing = $callback; return $this; } /** * @return \Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): ?(\Illuminate\Contracts\Auth\Authenticatable) */ public function getResolveUserUsing(): Closure { return $this->resolveUserUsing ?? function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) { /** @var \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable> $model */ $model = (new $this->userModelClass()); return $model->where( 'email', $oauthUser->getEmail() )->first(); }; } /** * @param ?\Closure(\DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin, \Laravel\Socialite\Contracts\User $oauthUser): bool $callback */ public function authorizeUserUsing(?Closure $callback = null): static { $this->authorizeUserUsing = $callback; return $this; } /** * @return \Closure(\DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin, \Laravel\Socialite\Contracts\User $oauthUser): bool */ public function getAuthorizeUserUsing(): Closure { return $this->authorizeUserUsing ?? static::checkDomainAllowList(...); } public static function checkDomainAllowList(FilamentSocialitePlugin $plugin, SocialiteUserContract $oauthUser): bool { $domains = $plugin->getDomainAllowList(); // When no domains are specified, all users are allowed if (count($domains) < 1) { return true; } // Get the domain of the email for the specified user $emailDomain = Str::of($oauthUser->getEmail() ?? throw new ImplementationException('User email is required.')) ->afterLast('@') ->lower() ->__toString(); // See if everything after @ is in the domains array return in_array($emailDomain, $domains); } } ================================================ FILE: src/Traits/CanBeHidden.php ================================================ isHidden = $condition; return $this; } public function visible(bool | Closure $condition = true): static { $this->isVisible = $condition; return $this; } public function isHidden(): bool { if ($this->evaluate($this->isHidden)) { return true; } return ! $this->evaluate($this->isVisible); } public function isVisible(): bool { return ! $this->isHidden(); } } ================================================ FILE: src/Traits/Models.php ================================================ */ protected string $userModelClass = \App\Models\User::class; /** * @var class-string<\DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser> */ protected string $socialiteUserModelClass = SocialiteUser::class; /** * @param class-string<\Illuminate\Contracts\Auth\Authenticatable> $value */ public function userModelClass(string $value): static { $this->userModelClass = $value; return $this; } /** * @return class-string<\Illuminate\Contracts\Auth\Authenticatable> */ public function getUserModelClass(): string { return $this->userModelClass; } /** * @param class-string<\DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser> $value */ public function socialiteUserModelClass(string $value): static { $this->socialiteUserModelClass = $value; return $this; } /** * @return class-string<\DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser> */ public function getSocialiteUserModelClass(): string { return $this->socialiteUserModelClass; } public function getSocialiteUserModel(): FilamentSocialiteUserContract { return new ($this->getSocialiteUserModelClass()); } } ================================================ FILE: src/Traits/Routes.php ================================================ getPanel()->generateRouteName('oauth.redirect')}"; } public function loginRouteName(string $value): static { $this->loginRouteName = $value; return $this; } public function getLoginRouteName(): string { return $this->loginRouteName ?? $this->getPanel()->generateRouteName('auth.login'); } public function dashboardRouteName(string $value): static { $this->dashboardRouteName = $value; return $this; } public function getDashboardRouteName(): string { return $this->dashboardRouteName ?? $this->getPanel()->generateRouteName('pages.dashboard'); } } ================================================ FILE: src/View/Components/Buttons.php ================================================ plugin = FilamentSocialitePlugin::current(); } /** * @inheritDoc */ public function render() { $messageBag = new MessageBag(); if (session()->has('filament-socialite-login-error')) { $messageBag->add('login-failed', session()->pull('filament-socialite-login-error')); } return view('filament-socialite::components.buttons', [ 'providers' => $providers = $this->plugin->getProviders(), 'visibleProviders' => array_filter($providers, fn (Provider $provider) => $provider->isVisible()), 'socialiteRoute' => $this->plugin->getRoute(), 'messageBag' => $messageBag, ]); } } ================================================ FILE: tailwind.config.js ================================================ module.exports = { content: ["./resources/views/**/*.blade.php"], corePlugins: { preflight: false, }, } ================================================ FILE: tests/Fixtures/TestSocialiteUser.php ================================================ email; } public function getAvatar() { return null; } } ================================================ FILE: tests/Fixtures/TestTeam.php ================================================ $teams */ class TestTenantUser extends TestUser implements HasTenants { public function canAccessTenant(Model $tenant): bool { return $this->teams->contains($tenant); } /** * @return \Illuminate\Support\Collection */ public function getTenants(Panel $panel): Collection { return $this->teams; } /** * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTeam, $this> */ public function teams(): BelongsToMany { return $this->belongsToMany(TestTeam::class, 'team_user', 'user_id', 'team_id'); } } ================================================ FILE: tests/Fixtures/TestUser.php ================================================ string('password')->nullable()->change(); }); } public function down(): void { // } }; ================================================ FILE: tests/Fixtures/create_socialite_users_table.php ================================================ id(); $table->foreignId('user_id'); $table->string('provider'); $table->string('provider_id'); $table->timestamps(); $table->unique([ 'provider', 'provider_id', ]); }); } public function down(): void { Schema::dropIfExists('socialite_users'); } }; ================================================ FILE: tests/Fixtures/create_team_user_table.php ================================================ integer('team_id'); $table->integer('user_id'); $table->unique(['team_id', 'user_id']); }); } public function down(): void { Schema::dropIfExists('team_user'); } }; ================================================ FILE: tests/Fixtures/create_teams_table.php ================================================ id(); $table->string('name'); }); } public function down(): void { Schema::dropIfExists('teams'); } }; ================================================ FILE: tests/SocialiteLoginAuthorizationTest.php ================================================ Panel::make() ->default() ->id($this::getPanelName()) ->path($this::getPanelName()) ->tenant(...$this->tenantArguments) ->login() ->pages([ Dashboard::class, ]) ->plugins([ FilamentSocialitePlugin::make() ->providers([ PluginProvider::make('github') ->label('GitHub') ->icon('fab-github') ->color('danger') ->outlined(false), PluginProvider::make('gitlab') ->label('GitLab') ->icon('fab-gitlab') ->color('danger') ->outlined() ->scopes([]) ->with([]), ]) ->registration(true) ->userModelClass($this->userModelClass) ->authorizeUserUsing(function (FilamentSocialitePlugin $plugin, SocialiteUserContract $oauthUser) { return $oauthUser->getEmail() === 'test@example.com'; }), ]), ); } #[DataProvider('loginDataProvider')] public function testAuthorizationLogin(string $email, bool $dispatchesUserNotAllowedEvent): void { Event::fake(); $response = $this ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.redirect", ['provider' => 'github'])) ->assertStatus(302); $state = session()->get('state'); $location = $response->headers->get('location') ?? throw new LogicException('Location header not set.'); parse_str($location, $urlQuery); // Test if the correct state is sent to the endpoint in the "Location" header. $this->assertEquals($state, $urlQuery['state']); // Assert decrypting of the state gives the correct panel name. $this->assertEquals($this::getPanelName(), Crypt::decrypt($state)); $user = new TestSocialiteUser(); $user->email = $email; Socialite::shouldReceive('driver') ->with('github') ->andReturn(static::makeOAuthProviderMock( request()->merge(['state' => $state]), $user )); // Fake oauth response. $response = $this ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.callback", ['provider' => 'github', 'state' => $state])) ->assertStatus(302); $dispatchesUserNotAllowedEvent ? Event::assertDispatched(UserNotAllowed::class) : Event::assertNotDispatched(UserNotAllowed::class); } /** * @return array */ public static function loginDataProvider(): array { return [ 'User is authorized to use the application so UserNotAllowedEvent should not be dispatched' => [ 'test@example.com', false, ], 'User is not authorized to use the application so UserNotAllowedEvent should be dispatched' => [ 'test@example1.com', true, ], ]; } } ================================================ FILE: tests/SocialiteLoginTest.php ================================================ getJson(route("socialite.filament.{$this::getPanelName()}.oauth.redirect", ['provider' => 'github'])) ->assertStatus(302); $state = session()->get('state'); $location = $response->headers->get('location') ?? throw new LogicException('Location header not set.'); parse_str($location, $urlQuery); // Test if the correct state is sent to the endpoint in the "Location" header. $this->assertEquals($state, $urlQuery['state']); // Assert decrypting of the state gives the correct panel name. $this->assertEquals($this::getPanelName(), Crypt::decrypt($state)); $user = new TestSocialiteUser(); $user->email = $email; Socialite::shouldReceive('driver') ->with('github') ->andReturn(static::makeOAuthProviderMock( request()->merge(['state' => $overrideState ?? $state]), $user )); // Fake oauth response. $response = $this ->getJson(route($callbackRoute, ['provider' => 'github', 'state' => $state])) ->assertStatus(302); if ($dispatchedErrorEvent) { Event::assertDispatched($dispatchedErrorEvent); $this->assertDatabaseMissing('socialite_users', [ 'provider' => 'github', 'provider_id' => 'test-socialite-user-id', ]); $this->assertDatabaseMissing('users', [ 'name' => 'test-socialite-user-name', 'email' => $user->email, ]); } else { $this->assertDatabaseHas('socialite_users', [ 'provider' => 'github', 'provider_id' => 'test-socialite-user-id', ]); $this->assertDatabaseHas('users', [ 'name' => 'test-socialite-user-name', 'email' => $user->email, ]); } } /** * @return array */ public static function loginDataProvider(): array { return [ 'Login fails when incorrect state (panelized callback route)' => [ 'test@example.com', // Use the new callback route that already contains the panel in the url. 'socialite.filament.'.static::getPanelName().'.oauth.callback', 'invalid-mocked-state', InvalidState::class, ], 'Login fails when incorrect state (general callback route)' => [ 'test@example.com', // Use the old callback route that determines the panel based on the state parameter. 'oauth.callback', 'invalid-mocked-state', InvalidState::class, ], 'Login succeeds when email in domain allow list' => [ 'test@example.com', 'socialite.filament.'.static::getPanelName().'.oauth.callback', null, null, ], 'Login fails when email not in domain allow list' => [ 'test@example1.com', 'socialite.filament.'.static::getPanelName().'.oauth.callback', null, UserNotAllowed::class, ], ]; } #[DataProvider('registrationBlockProvider')] public function testRegistrationBlock(bool $createUser, Closure | bool $registrationEnabled): void { Event::fake(); if ($createUser) { DB::table('users')->insert([ 'name' => 'test-user', 'email' => 'test@example.com', ]); } FilamentSocialitePlugin::current()->registration($registrationEnabled); $this ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.redirect", ['provider' => 'github'])) ->assertStatus(302); $state = session()->get('state'); Socialite::shouldReceive('driver') ->with('github') ->andReturn(static::makeOAuthProviderMock( request()->merge(['state' => $state]), new TestSocialiteUser() )); // Fake oauth response. $this ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.callback", ['provider' => 'github', 'state' => $state])) ->assertStatus(302); if (! $createUser) { // If there is no user, the event should have been dispatched since the plugin option disabled registration. Event::assertDispatched(RegistrationNotEnabled::class); } } /** * @return array> */ public static function registrationBlockProvider(): array { $callback = function (string $provider, SocialiteUserContract $oauthUser, ?Authenticatable $user) { return (bool) $user; }; return [ 'Authenticatable exists for socialite user' => [true, $callback], 'Authenticatable does not exist for socialite user' => [false, $callback], 'Registration is always blocked' => [true, false], ]; } } ================================================ FILE: tests/SocialiteStatelessLoginTest.php ================================================ Panel::make() ->default() ->id($this::getPanelName()) ->path($this::getPanelName()) ->tenant(...$this->tenantArguments) ->login() ->pages([ Dashboard::class, ]) ->plugins([ FilamentSocialitePlugin::make() ->providers([ Provider::make('github') ->label('GitHub') ->icon('fab-github') ->color('danger') ->outlined(false) ->stateless(), Provider::make('gitlab') ->label('GitLab') ->icon('fab-gitlab') ->color('danger') ->outlined() ->scopes([]) ->with([]), ]) ->registration(true) ->userModelClass($this->userModelClass) ->domainAllowList(['example.com']), ]), ); } #[DataProvider('statelessLoginDataProvider')] public function testStatelessLogin( string $email, string $callbackRoute, ?string $overrideState = null, ?string $event = null, ): void { Event::fake(); $response = $this ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.redirect", ['provider' => 'github'])) ->assertStatus(302); $state = session()->get('state'); $location = $response->headers->get('location') ?? throw new LogicException('Location header not set.'); parse_str($location, $urlQuery); // Test if the correct state is sent to the endpoint in the "Location" header. $this->assertEquals($state, $urlQuery['state']); // Assert decrypting of the state gives the correct panel name. $this->assertEquals($this::getPanelName(), Crypt::decrypt($state)); $user = new TestSocialiteUser(); $user->email = $email; Socialite::shouldReceive('driver') ->with('github') ->andReturn(static::makeOAuthProviderMock( request()->merge(['state' => $overrideState ?? $state]), $user )); // Fake oauth response. $response = $this ->getJson(route($callbackRoute, ['provider' => 'github', 'state' => $state])) ->assertStatus(302); if ($event !== null) { Event::assertNotDispatched($event); } $this->assertDatabaseHas('socialite_users', [ 'provider' => 'github', 'provider_id' => 'test-socialite-user-id', ]); $this->assertDatabaseHas('users', [ 'name' => 'test-socialite-user-name', 'email' => $user->email, ]); } /** * @return array */ public static function statelessLoginDataProvider(): array { return [ 'Stateless login succeeds (panelized callback route)' => [ 'test@example.com', // Use the new callback route that already contains the panel in the url. 'socialite.filament.'.static::getPanelName().'.oauth.callback', null, InvalidState::class, ], 'Stateless login succeeds with mocked state (general callback route)' => [ 'test@example.com', // Use the old callback route that determines the panel based on the state parameter. 'oauth.callback', 'invalid-mocked-state', InvalidState::class, ], ]; } } ================================================ FILE: tests/SocialiteTenantLoginTest.php ================================================ redirectAfterLoginUsing(function (string $provider, FilamentSocialiteUserContract $socialiteUser, FilamentSocialitePlugin $plugin) { assert($socialiteUser instanceof SocialiteUser); $this->assertEquals($this::getPanelName(), $plugin->getPanel()->getId()); $this->assertEquals('github', $provider); $this->assertEquals('github', $socialiteUser->provider); $this->assertEquals('test-socialite-user-id', $socialiteUser->provider_id); return redirect()->to('/some-tenant-url'); }); $response = $this ->getJson("/{$this::getPanelName()}/oauth/github") ->assertStatus(302); $state = session()->get('state'); Socialite::shouldReceive('driver') ->with('github') ->andReturn(static::makeOAuthProviderMock( request()->merge(['state' => $state]), new TestSocialiteUser() )); // Fake oauth response. $response = $this ->getJson("/oauth/callback/github?state=$state") ->assertStatus(302); $this->assertStringContainsString('/some-tenant-url', $response->headers->get('Location') ?? throw new LogicException('Location header not set.')); $this->assertDatabaseHas('socialite_users', [ 'provider' => 'github', 'provider_id' => 'test-socialite-user-id', ]); $this->assertDatabaseHas('users', [ 'name' => 'test-socialite-user-name', 'email' => 'test@example.com', ]); } } ================================================ FILE: tests/TestCase.php ================================================ */ protected string $userModelClass = TestUser::class; /** * @var array{0: ?class-string<\Illuminate\Database\Eloquent\Model>, 1?: ?string, 2?: ?string} */ protected array $tenantArguments = [null]; protected function setUp(): void { parent::setUp(); Filament::setCurrentPanel(self::getPanelName()); Factory::guessFactoryNamesUsing( fn ( string $modelName, ) => 'DutchCodingCompany\\FilamentSocialite\\Database\\Factories\\'.class_basename($modelName).'Factory' ); $this->app?->make(Kernel::class)->pushMiddleware(StartSession::class); } protected function getPackageProviders($app) { $this->registerTestPanel(); return [ FilamentServiceProvider::class, FilamentSocialiteServiceProvider::class, SocialiteServiceProvider::class, LivewireServiceProvider::class, ]; } protected function registerTestPanel(): void { Filament::registerPanel( fn (): Panel => Panel::make() ->default() ->id($this::getPanelName()) ->path($this::getPanelName()) ->tenant(...$this->tenantArguments) ->login() ->pages([ Dashboard::class, ]) ->plugins([ FilamentSocialitePlugin::make() ->providers([ Provider::make('github') ->label('GitHub') ->icon('fab-github') ->color('danger') ->outlined(false), Provider::make('gitlab') ->label('GitLab') ->icon('fab-gitlab') ->color('danger') ->outlined() ->scopes([]) ->with([]), ]) ->registration(true) ->userModelClass($this->userModelClass) ->domainAllowList(['example.com']), ]), ); } public function getEnvironmentSetUp($app) { config()->set('app.key', 'base64:'.base64_encode( Encrypter::generateKey('AES-256-CBC') )); config()->set('services.github', [ 'client_id' => 'abcdmockedabcd', 'client_secret' => 'defgmockeddefg', 'redirect' => 'http://localhost/oauth/callback/github', ]); } protected function defineDatabaseMigrations() { $this->loadLaravelMigrations(); $this->loadMigrationsFrom(__DIR__.'/Fixtures'); } protected static function getPanelName(): string { return 'testpanel'; } protected static function makeOAuthProviderMock( Request $request, SocialiteUserContract $user, ): LegacyMockInterface { $mock = \Mockery::mock( AbstractProvider::class, [$request, 'test-client-id', 'test-client-secret', 'test-redirect-url'] ) ->makePartial() ->shouldAllowMockingProtectedMethods(); $mock->shouldReceive('getAccessTokenResponse')->andReturn([]); $mock->shouldReceive('getUserByToken')->andReturn([]); $mock->shouldReceive('userInstance')->andReturn($user); return $mock; } }