Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@
"redirect" => env("SIGN_IN_WITH_APPLE_REDIRECT"),
"client_id" => env("SIGN_IN_WITH_APPLE_CLIENT_ID"),
"client_secret" => env("SIGN_IN_WITH_APPLE_CLIENT_SECRET"),
// Auto-register package routes
"routes" => [
"enabled" => true,
"redirect_route" => "apple/redirect",
"callback_route" => "apple/callback",
"callback_redirect" => "/",
],
],
];
19 changes: 19 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

use GeneaLabs\LaravelSignInWithApple\Http\Controllers\AppleSignInController;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Support\Facades\Route;

Route::group([
'middleware' => ['web'],
], function () {
$redirectPath = config('services.sign_in_with_apple.routes.redirect_route', 'apple/redirect');
$callbackPath = config('services.sign_in_with_apple.routes.callback_route', 'apple/callback');

Route::get($redirectPath, [AppleSignInController::class, 'redirect'])
->name('apple.redirect');

Route::post($callbackPath, [AppleSignInController::class, 'callback'])
->withoutMiddleware([VerifyCsrfToken::class])
->name('apple.callback');
});
18 changes: 18 additions & 0 deletions src/Events/AppleSignInCallback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace GeneaLabs\LaravelSignInWithApple\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Laravel\Socialite\Contracts\User;

class AppleSignInCallback
{
use Dispatchable;
use SerializesModels;

public function __construct(
public readonly User $user,
) {
}
}
43 changes: 43 additions & 0 deletions src/Http/Controllers/AppleSignInController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace GeneaLabs\LaravelSignInWithApple\Http\Controllers;

use GeneaLabs\LaravelSignInWithApple\Events\AppleSignInCallback;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Laravel\Socialite\Facades\Socialite;

class AppleSignInController extends Controller
{
/**
* Redirect to Apple for authentication.
*/
public function redirect(): RedirectResponse
{
return Socialite::driver('sign-in-with-apple')
->scopes(['name', 'email'])
->redirect();
}

/**
* Handle the callback from Apple.
*
* Dispatches an AppleSignInCallback event with the Socialite user,
* then redirects to a configurable route. Listen for the event
* to persist or process the authenticated user.
*/
public function callback(Request $request): RedirectResponse
{
$user = Socialite::driver('sign-in-with-apple')->user();

AppleSignInCallback::dispatch($user);

$redirect = config(
'services.sign_in_with_apple.routes.callback_redirect',
'/',
);

return redirect()->to($redirect);
}
}
11 changes: 11 additions & 0 deletions src/Providers/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ServiceProvider extends LaravelServiceProvider

public function boot()
{
$this->loadRoutesIf(config('services.sign_in_with_apple.routes.enabled', true));
$this->bootSocialiteDriver();
$this->bootBladeDirective();
}
Expand All @@ -30,6 +31,16 @@ protected function registerConfiguration()
);
}

/**
* Load package routes if enabled in config.
*/
protected function loadRoutesIf(bool $enabled): void
{
if ($enabled) {
$this->loadRoutesFrom(__DIR__ . '/../../routes/web.php');
}
}

public function bootSocialiteDriver()
{
$socialite = $this->app->make(Factory::class);
Expand Down
146 changes: 146 additions & 0 deletions tests/Unit/RouteRegistrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

namespace GeneaLabs\LaravelSignInWithApple\Tests\Unit;

use GeneaLabs\LaravelSignInWithApple\Http\Controllers\AppleSignInController;
use GeneaLabs\LaravelSignInWithApple\Tests\UnitTestCase;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;

class RouteRegistrationTest extends UnitTestCase
{
public function testAppleRedirectRouteIsRegistered(): void
{
$routes = $this->app['router']->getRoutes();

$redirectRoute = $routes->getByName('apple.redirect');

$this->assertNotNull($redirectRoute);
$this->assertEquals('apple/redirect', $redirectRoute->uri);
$this->assertTrue(in_array('GET', $redirectRoute->methods));
}

public function testAppleCallbackRouteIsRegistered(): void
{
$routes = $this->app['router']->getRoutes();

$callbackRoute = $routes->getByName('apple.callback');

$this->assertNotNull($callbackRoute);
$this->assertEquals('apple/callback', $callbackRoute->uri);
$this->assertTrue(in_array('POST', $callbackRoute->methods));
}

public function testRedirectRouteUsesAppleSignInController(): void
{
$routes = $this->app['router']->getRoutes();
$route = $routes->getByName('apple.redirect');

$controller = $route->getAction('controller');
$this->assertStringContainsString('AppleSignInController', $controller);
$this->assertStringContainsString('redirect', $controller);
}

public function testCallbackRouteUsesAppleSignInController(): void
{
$routes = $this->app['router']->getRoutes();
$route = $routes->getByName('apple.callback');

$controller = $route->getAction('controller');
$this->assertStringContainsString('AppleSignInController', $controller);
$this->assertStringContainsString('callback', $controller);
}

public function testRoutesAreProtectedWithWebMiddleware(): void
{
$routes = $this->app['router']->getRoutes();

$redirectRoute = $routes->getByName('apple.redirect');
$callbackRoute = $routes->getByName('apple.callback');

$this->assertTrue(in_array('web', $redirectRoute->middleware()));
$this->assertTrue(in_array('web', $callbackRoute->middleware()));
}

public function testCallbackRouteExcludesCsrfVerification(): void
{
$routes = $this->app['router']->getRoutes();
$callbackRoute = $routes->getByName('apple.callback');

$excludedMiddleware = $callbackRoute->excludedMiddleware();
$this->assertContains(VerifyCsrfToken::class, $excludedMiddleware);
}

public function testRoutesAreNotRegisteredWhenDisabled(): void
{
$this->app['config']->set('services.sign_in_with_apple.routes.enabled', false);

// Clear routes and re-boot the service provider with routes disabled
$this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection());

$provider = new \GeneaLabs\LaravelSignInWithApple\Providers\ServiceProvider($this->app);
$provider->boot();

$routes = $this->app['router']->getRoutes();
$routes->refreshNameLookups();

$this->assertNull($routes->getByName('apple.redirect'));
$this->assertNull($routes->getByName('apple.callback'));
}

public function testRedirectRoutePathCanBeCustomized(): void
{
$this->app['config']->set('services.sign_in_with_apple.routes.redirect_route', 'auth/apple/login');

$this->reloadRoutes();

$routes = $this->app['router']->getRoutes();
$redirectRoute = $routes->getByName('apple.redirect');

$this->assertNotNull($redirectRoute);
$this->assertEquals('auth/apple/login', $redirectRoute->uri);
}

public function testCallbackRoutePathCanBeCustomized(): void
{
$this->app['config']->set('services.sign_in_with_apple.routes.callback_route', 'auth/apple/handle');

$this->reloadRoutes();

$routes = $this->app['router']->getRoutes();
$callbackRoute = $routes->getByName('apple.callback');

$this->assertNotNull($callbackRoute);
$this->assertEquals('auth/apple/handle', $callbackRoute->uri);
}

/**
* Clear existing routes and reload the package route file.
*/
private function reloadRoutes(): void
{
$this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection());

require __DIR__ . '/../../routes/web.php';

$this->app['router']->getRoutes()->refreshNameLookups();
}

public function testControllerCanBeOverriddenByApplication(): void
{
$this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection());

// Simulate an app overriding the callback route with a custom controller
$this->app['router']->group(['middleware' => ['web']], function ($router) {
$router->post('apple/callback', [\GeneaLabs\LaravelSignInWithApple\Tests\Fixtures\Http\Controllers\SiwaController::class, 'callback'])
->name('apple.callback');
});

$routes = $this->app['router']->getRoutes();
$routes->refreshNameLookups();
$callbackRoute = $routes->getByName('apple.callback');

$this->assertNotNull($callbackRoute);
$controller = $callbackRoute->getAction('controller');
$this->assertStringContainsString('SiwaController', $controller);
}
}
Loading