Skip to content

Commit

Permalink
Merge pull request #105 from DutchCodingCompany/improve-routing
Browse files Browse the repository at this point in the history
Add new callback route
  • Loading branch information
bert-w authored Jul 15, 2024
2 parents 06984df + 413485e commit dac777f
Show file tree
Hide file tree
Showing 14 changed files with 316 additions and 115 deletions.
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ use Illuminate\Contracts\Auth\Authenticatable;
->scopes(['...'])
->with(['...']),
])
// (optional) Override the panel slug to be used in the oauth routes. Defaults to the panel ID.
->slug('admin')
// (optional) Enable/disable registration of new (socialite-) users.
->registration(true)
// (optional) Enable/disable registration of new (socialite-) users using a callback.
Expand All @@ -77,13 +79,46 @@ use Illuminate\Contracts\Auth\Authenticatable;
);
```

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.

### CSRF protection
_(Laravel 11.x users can ignore this section)_

If your third-party provider calls the OAuth callback using a `POST` request, you need to add the callback route to the
exception list in your `VerifyCsrfToken` middleware. This can be done by adding the url to the `$except` array:

```php
protected $except = [
'*/oauth/callback/*',
'oauth/callback/*',
];
````

For Laravel 11.x users, this exception is automatically added by our service provider.

See [Socialite Providers](https://socialiteproviders.com/) for additional Socialite providers.

### 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
```

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
],
"require": {
"php": "^8.1",
"filament/filament": "^3.0.9",
"filament/filament": "^3.2.72",
"illuminate/contracts": "^10.0|^11.0",
"laravel/socialite": "^5.5",
"spatie/laravel-package-tools": "^1.9.2"
Expand Down
19 changes: 0 additions & 19 deletions database/factories/ModelFactory.php

This file was deleted.

19 changes: 2 additions & 17 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,15 @@ parameters:
count: 1
path: src/FilamentSocialitePlugin.php

-
message: "#^Property DutchCodingCompany\\\\FilamentSocialite\\\\FilamentSocialitePlugin\\:\\:\\$userModelClass \\(class\\-string\\<Illuminate\\\\Contracts\\\\Auth\\\\Authenticatable\\>\\) does not accept default value of type string\\.$#"
count: 1
path: src/FilamentSocialitePlugin.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\\.$#"
count: 1
path: src/Http/Controllers/SocialiteLoginController.php

-
message: "#^Call to an undefined method Mockery\\\\ExpectationInterface\\|Mockery\\\\HigherOrderMessage\\:\\:andReturn\\(\\)\\.$#"
count: 1
path: tests/SocialiteLoginAuthorizationTest.php

-
message: "#^Call to an undefined method Mockery\\\\ExpectationInterface\\|Mockery\\\\HigherOrderMessage\\:\\:andReturn\\(\\)\\.$#"
count: 2
path: tests/SocialiteLoginTest.php

-
message: "#^Call to an undefined method Mockery\\\\ExpectationInterface\\|Mockery\\\\HigherOrderMessage\\:\\:andReturn\\(\\)\\.$#"
count: 1
path: tests/SocialiteTenantLoginTest.php
count: 3
path: tests/TestCase.php

-
message: "#^Parameter \\#1 \\$callback of static method Illuminate\\\\Database\\\\Eloquent\\\\Factories\\\\Factory\\<Illuminate\\\\Database\\\\Eloquent\\\\Model\\>\\:\\:guessFactoryNamesUsing\\(\\) expects callable\\(class\\-string\\<Illuminate\\\\Database\\\\Eloquent\\\\Model\\>\\)\\: class\\-string\\<Illuminate\\\\Database\\\\Eloquent\\\\Factories\\\\Factory\\>, Closure\\(string\\)\\: non\\-falsy\\-string given\\.$#"
Expand Down
18 changes: 16 additions & 2 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,29 @@
$domains = $panel->getDomains();

foreach ((empty($domains) ? [null] : $domains) as $domain) {
$redirectRoute = "socialite.{$panel->generateRouteName('oauth.redirect')}";
Filament::currentDomain($domain);

Route::domain($domain)
->middleware($panel->getMiddleware())
->name($redirectRoute)
->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,
Expand Down
13 changes: 11 additions & 2 deletions src/FilamentSocialiteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

namespace DutchCodingCompany\FilamentSocialite;

use DutchCodingCompany\FilamentSocialite\Exceptions\ImplementationException;
use DutchCodingCompany\FilamentSocialite\View\Components\Buttons;
use Filament\Facades\Filament;
use Filament\Panel;
use Filament\Support\Facades\FilamentView;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Support\Facades\Blade;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
Expand Down Expand Up @@ -49,5 +48,15 @@ static function (): ?string {
return Blade::render('<x-filament-socialite::buttons :show-divider="'.($plugin->getShowDivider() ? 'true' : 'false').'" />');
},
);

if (
version_compare(app()->version(), '11.0', '>=')
&& method_exists(VerifyCsrfToken::class, 'except')
) {
VerifyCsrfToken::except([
'*/oauth/callback/*',
'oauth/callback/*',
]);
}
}
}
5 changes: 5 additions & 0 deletions src/Http/Middleware/PanelFromUrlQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;

/**
* @note This callback uses the `state` input to determine the correct panel ID. A simpler
* implementation is to use the "$slug/oauth/callback/{provider}" route instead, which
* contains the panel ID in the url itself.
*/
class PanelFromUrlQuery
{
public function handle(Request $request, Closure $next): mixed
Expand Down
10 changes: 5 additions & 5 deletions src/Models/SocialiteUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;
Expand All @@ -17,20 +16,21 @@
*/
class SocialiteUser extends Model implements FilamentSocialiteUserContract
{
use HasFactory;

protected $fillable = [
'user_id',
'provider',
'provider_id',
];

/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable, self>
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Illuminate\Database\Eloquent\Model, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(FilamentSocialitePlugin::current()->getUserModelClass());
/** @var class-string<\Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable> */
$user = FilamentSocialitePlugin::current()->getUserModelClass();

return $this->belongsTo($user);
}

public function getUser(): Authenticatable
Expand Down
9 changes: 6 additions & 3 deletions src/Traits/Models.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
trait Models
{
/**
* @var class-string<\Illuminate\Contracts\Auth\Authenticatable>
* @var ?class-string<\Illuminate\Contracts\Auth\Authenticatable>
*/
protected string $userModelClass = User::class;
protected ?string $userModelClass = null;

/**
* @var class-string<\DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser>
Expand All @@ -40,7 +40,10 @@ public function userModelClass(string $value): static
*/
public function getUserModelClass(): string
{
return $this->userModelClass;
/** @var class-string<\Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable> */
$user = User::class;

return $this->userModelClass ?? $user;
}

/**
Expand Down
20 changes: 9 additions & 11 deletions tests/SocialiteLoginAuthorizationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ protected function registerTestPanel(): void
Filament::registerPanel(
fn (): Panel => Panel::make()
->default()
->id($this->panelName)
->path($this->panelName)
->id($this::getPanelName())
->path($this::getPanelName())
->tenant(...$this->tenantArguments)
->login()
->pages([
Expand Down Expand Up @@ -63,7 +63,7 @@ public function testAuthorizationLogin(string $email, bool $dispatchesUserNotAll
Event::fake();

$response = $this
->getJson("/$this->panelName/oauth/github")
->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.redirect", ['provider' => 'github']))
->assertStatus(302);

$state = session()->get('state');
Expand All @@ -76,23 +76,21 @@ public function testAuthorizationLogin(string $email, bool $dispatchesUserNotAll
$this->assertEquals($state, $urlQuery['state']);

// Assert decrypting of the state gives the correct panel name.
$this->assertEquals($this->panelName, Crypt::decrypt($state));
$this->assertEquals($this::getPanelName(), Crypt::decrypt($state));

$user = new TestSocialiteUser();
$user->email = $email;

Socialite::shouldReceive('driver')
->with('github')
->andReturn(
Mockery::mock(Provider::class)
->shouldReceive('user')
->andReturn($user)
->getMock()
);
->andReturn(static::makeOAuthProviderMock(
request()->merge(['state' => $state]),
$user
));

// Fake oauth response.
$response = $this
->getJson("/oauth/callback/github?state=$state")
->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.callback", ['provider' => 'github', 'state' => $state]))
->assertStatus(302);

$dispatchesUserNotAllowedEvent
Expand Down
Loading

0 comments on commit dac777f

Please sign in to comment.