From 24bdaf72e2716d01d700870e9896c3ed47bb9176 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Tue, 19 May 2026 14:51:59 -0400 Subject: [PATCH 01/22] Address principalName / email details (Port of CD-LaravelStarterKi:42ffa341) --- src/DataObjects/RemoteIdentity.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/DataObjects/RemoteIdentity.php b/src/DataObjects/RemoteIdentity.php index fd6a6c2..3fdd2bd 100644 --- a/src/DataObjects/RemoteIdentity.php +++ b/src/DataObjects/RemoteIdentity.php @@ -7,6 +7,7 @@ public function __construct( public string $idp, public string $uid, + public string $principalName = '', public string $displayName = '', public string $email = '', public array $data = [], @@ -26,12 +27,13 @@ public static function fromData( return new RemoteIdentity( idp: $idp, uid: $uid, + principalName: $eduPersonPrincipalName + ?? $mail + ?? $uid, displayName: $displayName ?? $cn ?? trim(($givenName ?? '').' '.($sn ?? '')), - email: $eduPersonPrincipalName - ?? $mail - ?? '', + email: $mail ?: null, data: $data, ); } @@ -55,12 +57,20 @@ public function uniqueUid(): string }; } + /* + * Returns the eduPersonPrincipalName + */ + public function principalName(): string + { + return $this->principalName; + } + /** * Returns the primary email (netid@cornell.edu|cwid@med.cornell.edu) if available, otherwise the alias email. */ public function email(): string { - return $this->email; + return $this->principalName ?: $this->email; } /** From e373e1cb051bb5d27a3be690c9841bdae10a112c Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Tue, 19 May 2026 15:11:56 -0400 Subject: [PATCH 02/22] Align composer.json and configurations --- .editorconfig | 3 -- .gitattributes | 1 - .github/workflows/phpunit.yml | 31 ++++++------- composer.json | 83 +++++++++++++++++++++-------------- phpunit.xml | 11 +++-- 5 files changed, 71 insertions(+), 58 deletions(-) diff --git a/.editorconfig b/.editorconfig index 4e4e962..dd9a2b5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,3 @@ trim_trailing_whitespace = false [*.{yml,yaml}] indent_size = 2 - -[*.json] -indent_size = 2 diff --git a/.gitattributes b/.gitattributes index e74ae9a..58d0cad 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,7 +6,6 @@ /.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.lando.yml export-ignore /phpunit.xml export-ignore /pint.json export-ignore /tests export-ignore diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 91bc61c..13eeadb 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -6,36 +6,33 @@ jobs: strategy: fail-fast: true matrix: - php: [8.2, 8.3, 8.4, 8.5] - laravel: [11.*, 12.*] - stability: [prefer-stable] + php: [8.3, 8.4, 8.5] + laravel: [12.*, 13.*] include: - - laravel: 11.* - testbench: 9.* - laravel: 12.* - testbench: 10.* + testbench: "^10.0" + - laravel: 13.* + testbench: "^11.0" + exclude: + - php: 8.3 + laravel: 13.* - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} + name: P${{ matrix.php }} - L${{ matrix.laravel }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite3, pdo_sqlite, bcmath, soap, intl, fileinfo + extensions: dom, curl, mbstring, pdo, bcmath, 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 + uses: ramsey/composer-install@v4 + with: + composer-options: "--no-cache --with=orchestra/testbench:${{ matrix.testbench }}" - name: List Installed Dependencies run: composer show -D diff --git a/composer.json b/composer.json index 29c7ec5..a546530 100644 --- a/composer.json +++ b/composer.json @@ -1,35 +1,52 @@ { - "name": "cornell-custom-dev/laravel-cu-auth", - "description": "A Laravel package for authentication and identity management at Cornell University", - "license": "MIT", - "type": "library", - "require": { - "php": "^8.2", - "ext-openssl": "*", - "illuminate/support": "^11.0|^12.0", - "onelogin/php-saml": "^4.3.1" - }, - "require-dev": { - "laravel/pint": "^1.20", - "orchestra/testbench": "^9.0|^10.0", - "phpunit/phpunit": "^10.5|^11.5" - }, - "autoload": { - "psr-4": { - "CornellCustomDev\\LaravelStarterKit\\CUAuth\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "CornellCustomDev\\LaravelStarterKit\\CUAuth\\Tests\\": "tests" - } - }, - "extra": { - "laravel": { - "providers": [ - "CornellCustomDev\\LaravelStarterKit\\CUAuth\\CUAuthServiceProvider" - ] - } - }, - "prefer-stable": true + "name": "cornell-custom-dev/laravel-cu-auth", + "description": "A Laravel package for authentication and identity management at Cornell University", + "license": "MIT", + "type": "library", + "require": { + "php": "^8.3", + "ext-openssl": "*", + "illuminate/support": "^12.0|^13.0", + "onelogin/php-saml": "^4.3.1" + }, + "require-dev": { + "laravel/pint": "^1.20", + "orchestra/testbench": "^10.0|^11.0", + "phpunit/phpunit": "^11.5|^12.5" + }, + "autoload": { + "psr-4": { + "CornellCustomDev\\LaravelStarterKit\\CUAuth\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "CornellCustomDev\\LaravelStarterKit\\CUAuth\\Tests\\": "tests" + } + }, + "scripts": { + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "test": "@php vendor/bin/phpunit -c ./ --color", + "sync": "@php bin/sync", + "ci": [ + "@post-autoload-dump", + "@test" + ] + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "CornellCustomDev\\LaravelStarterKit\\CUAuth\\CUAuthServiceProvider" + ] + } + }, + "prefer-stable": true } diff --git a/phpunit.xml b/phpunit.xml index e85414b..805fa72 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,11 +1,14 @@ From 38cd0d6aac6fe34b43ba8a706c7c6014270c2241 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Tue, 19 May 2026 16:13:29 -0400 Subject: [PATCH 03/22] Fix test deprecations --- src/DataObjects/RemoteIdentity.php | 2 +- tests/Feature/ShibIdentityManagerTest.php | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/DataObjects/RemoteIdentity.php b/src/DataObjects/RemoteIdentity.php index 3fdd2bd..024d7c1 100644 --- a/src/DataObjects/RemoteIdentity.php +++ b/src/DataObjects/RemoteIdentity.php @@ -33,7 +33,7 @@ public static function fromData( displayName: $displayName ?? $cn ?? trim(($givenName ?? '').' '.($sn ?? '')), - email: $mail ?: null, + email: $mail ?? '', data: $data, ); } diff --git a/tests/Feature/ShibIdentityManagerTest.php b/tests/Feature/ShibIdentityManagerTest.php index 702cb8f..0702449 100644 --- a/tests/Feature/ShibIdentityManagerTest.php +++ b/tests/Feature/ShibIdentityManagerTest.php @@ -9,6 +9,7 @@ use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\CUAuth; use Illuminate\Http\Request; use Illuminate\Support\Facades\Event; +use Orchestra\Testbench\Attributes\DefineRoute; class ShibIdentityManagerTest extends FeatureTestCase { @@ -100,7 +101,7 @@ public function testCanFailAuthorizing() config(['cu-auth.require_local_user' => true]); $this->addCUAuthenticatedListener(authorized: false); $request = $this->getApacheAuthRequest('new-user'); - $identityManager = $this->createMock(ShibIdentityManager::class); + $identityManager = $this->createStub(ShibIdentityManager::class); $identityManager->method('hasIdentity')->willReturn(true); $response = (new CUAuth($identityManager)) @@ -152,9 +153,7 @@ protected static function usesAuthRoutes($router): void $router->get(ShibIdentityManager::SHIB_LOGIN_URL, fn () => 'ShibUrl'); } - /** - * @define-route usesAuthRoutes - */ + #[DefineRoute('usesAuthRoutes')] public function testRoutesAreProtected() { $this->get(route('test'))->assertOk(); @@ -162,7 +161,7 @@ public function testRoutesAreProtected() $this->followingRedirects()->get(route('test.require-cu-auth'))->assertSee('ShibUrl'); } - /** @define-route usesAuthRoutes */ + #[DefineRoute('usesAuthRoutes')] public function testRouteIsProtectedForRemoteUser() { $this->addCUAuthenticatedListener(); @@ -176,7 +175,7 @@ public function testRouteIsProtectedForRemoteUser() $this->followingRedirects()->get(route('test.require-cu-auth'))->assertOk(); } - /** @define-route usesAuthRoutes */ + #[DefineRoute('usesAuthRoutes')] public function testRouteIsProtectedForProductionRemoteUser() { $this->addCUAuthenticatedListener(); @@ -191,7 +190,7 @@ public function testRouteIsProtectedForProductionRemoteUser() $this->followingRedirects()->get(route('test.require-cu-auth'))->assertOk(); } - /** @define-route usesAuthRoutes */ + #[DefineRoute('usesAuthRoutes')] public function testRouteIsProtectedForLocalUser() { config(['cu-auth.apache_shib_user_variable' => 'REMOTE_USER_TEST']); @@ -213,7 +212,7 @@ public function testRouteIsProtectedWithoutUserLookup() $this->addCUAuthenticatedListener(authorized: false); $request = $this->getApacheAuthRequest('new-user'); - $identityManager = $this->createMock(ShibIdentityManager::class); + $identityManager = $this->createStub(ShibIdentityManager::class); $identityManager->method('hasIdentity')->willReturn(true); $response = (new CUAuth($identityManager))->handle($request, fn () => response('OK')); From 8c6d1af0a35edfd1838fb9708de575a1a7fc44f3 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Tue, 19 May 2026 17:14:03 -0400 Subject: [PATCH 04/22] Deprecate usage of uniqueUid --- src/DataObjects/RemoteIdentity.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/DataObjects/RemoteIdentity.php b/src/DataObjects/RemoteIdentity.php index 024d7c1..cfa53c6 100644 --- a/src/DataObjects/RemoteIdentity.php +++ b/src/DataObjects/RemoteIdentity.php @@ -48,6 +48,9 @@ public function id(): string /** * Provides an id that is unique across Cornell IdPs. + * + * @deprecated Use of uniqueUid() should be replaced with use of principalName() for a unique identifier + * across IdPs, and id() for an identifier within the IdP. */ public function uniqueUid(): string { @@ -83,11 +86,13 @@ public function name(): string public function isCornellIdP(): bool { - return str_contains($this->idp, 'cit.cornell.edu'); + return str_contains($this->idp, 'cit.cornell.edu') + || str_contains($this->principalName, '@cornell.edu'); } public function isWeillIdP(): bool { - return str_contains($this->idp, 'weill.cornell.edu'); + return str_contains($this->idp, 'weill.cornell.edu') + || str_contains($this->principalName, '@med.cornell.edu'); } } From b49bcf70ea2be2ff5d1fb3f333d727e3d351e43c Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Tue, 19 May 2026 17:15:06 -0400 Subject: [PATCH 05/22] Use mail instead of email internally for RemoteIdentity clarity --- src/DataObjects/RemoteIdentity.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/DataObjects/RemoteIdentity.php b/src/DataObjects/RemoteIdentity.php index cfa53c6..df8e5b1 100644 --- a/src/DataObjects/RemoteIdentity.php +++ b/src/DataObjects/RemoteIdentity.php @@ -10,6 +10,7 @@ public function __construct( public string $principalName = '', public string $displayName = '', public string $email = '', + private string $mail = '', public array $data = [], ) {} @@ -33,7 +34,8 @@ public static function fromData( displayName: $displayName ?? $cn ?? trim(($givenName ?? '').' '.($sn ?? '')), - email: $mail ?? '', + email: trim($mail ?? ''), + mail: trim($mail ?? ''), data: $data, ); } @@ -73,7 +75,7 @@ public function principalName(): string */ public function email(): string { - return $this->principalName ?: $this->email; + return $this->principalName ?: $this->mail; } /** From 5010c051ee2ed4a63f0a27cd0eabbcfb08408ac6 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Tue, 19 May 2026 17:15:31 -0400 Subject: [PATCH 06/22] RemoteIdentity->principleName shouldn't use uid --- src/DataObjects/RemoteIdentity.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DataObjects/RemoteIdentity.php b/src/DataObjects/RemoteIdentity.php index df8e5b1..fab9bed 100644 --- a/src/DataObjects/RemoteIdentity.php +++ b/src/DataObjects/RemoteIdentity.php @@ -29,8 +29,7 @@ public static function fromData( idp: $idp, uid: $uid, principalName: $eduPersonPrincipalName - ?? $mail - ?? $uid, + ?? trim($mail ?? ''), displayName: $displayName ?? $cn ?? trim(($givenName ?? '').' '.($sn ?? '')), From 794fa4626f84b9389e43d3e54782a753ff1bc1de Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Tue, 19 May 2026 17:16:16 -0400 Subject: [PATCH 07/22] Github workflow extension adjustments Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/phpunit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 13eeadb..4bd4d75 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -26,7 +26,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, mbstring, pdo, bcmath, fileinfo + extensions: dom, curl, mbstring, pdo, pdo_sqlite, sqlite3, bcmath, fileinfo coverage: none - name: Install dependencies From 79372b16bc7fdd3aaef5bd3e02aea6b0678eb762 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Tue, 19 May 2026 17:28:19 -0400 Subject: [PATCH 08/22] Add LivewireAuth --- config/cu-auth.php | 11 +++++++++++ src/CUAuthServiceProvider.php | 12 ++++++++++++ src/Middleware/LivewireAuth.php | 30 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 src/Middleware/LivewireAuth.php diff --git a/config/cu-auth.php b/config/cu-auth.php index 205d5d4..4063717 100644 --- a/config/cu-auth.php +++ b/config/cu-auth.php @@ -79,4 +79,15 @@ | */ 'allow_local_login' => boolval(env('ALLOW_LOCAL_LOGIN', false)), + + /* + |-------------------------------------------------------------------------- + | Restrict Access for Livewire Updates + |-------------------------------------------------------------------------- + | + | Add global middleware protection against unauthenticated posting to + | /livewire/update. + | + */ + 'require_livewire_auth' => boolval(env('REQUIRE_LIVEWIRE_AUTH', false)), ]; diff --git a/src/CUAuthServiceProvider.php b/src/CUAuthServiceProvider.php index a194c1b..15b6923 100644 --- a/src/CUAuthServiceProvider.php +++ b/src/CUAuthServiceProvider.php @@ -5,6 +5,8 @@ use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\IdentityManager; use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\SamlIdentityManager; use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\ShibIdentityManager; +use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\LivewireAuth; +use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; class CUAuthServiceProvider extends ServiceProvider @@ -51,5 +53,15 @@ public function boot(): void ]); } $this->loadRoutesFrom(__DIR__.'/../routes/cu-auth.php'); + + if (config('cu-auth.require_livewire_auth')) { + if (class_exists('Livewire')) { + \Livewire::setUpdateRoute(function ($handle) { + // Only logged in users can post data to livewire components + return Route::post('/livewire/update', $handle) + ->middleware(['web', LivewireAuth::class]); + }); + } + } } } diff --git a/src/Middleware/LivewireAuth.php b/src/Middleware/LivewireAuth.php new file mode 100644 index 0000000..4db53e3 --- /dev/null +++ b/src/Middleware/LivewireAuth.php @@ -0,0 +1,30 @@ +isMethod('POST') && $request->path() === 'livewire/update') { + if (! $this->identityManager->hasIdentity()) { + if (app()->runningInConsole()) { + return response('Forbidden', Response::HTTP_FORBIDDEN); + } + abort(403); + } + } + + return $next($request); + } +} From 6cdbffdb7198af318d04d1af6df96aaa8281cd97 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Wed, 20 May 2026 18:31:48 -0400 Subject: [PATCH 09/22] Deprecate IdentityManager::hasIdentity(), renamed to hasRemoteIdentity() --- src/Managers/IdentityManager.php | 2 +- src/Managers/SamlIdentityManager.php | 10 +++++++++- src/Managers/ShibIdentityManager.php | 10 +++++++++- src/Middleware/AppTesters.php | 2 +- src/Middleware/CUAuth.php | 3 ++- tests/Feature/ShibIdentityManagerTest.php | 4 ++-- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Managers/IdentityManager.php b/src/Managers/IdentityManager.php index 61aaa6b..8beb7e4 100644 --- a/src/Managers/IdentityManager.php +++ b/src/Managers/IdentityManager.php @@ -7,7 +7,7 @@ interface IdentityManager { - public function hasIdentity(): bool; + public function hasRemoteIdentity(): bool; public function getIdentity(): ?RemoteIdentity; diff --git a/src/Managers/SamlIdentityManager.php b/src/Managers/SamlIdentityManager.php index 621824d..4284877 100644 --- a/src/Managers/SamlIdentityManager.php +++ b/src/Managers/SamlIdentityManager.php @@ -57,11 +57,19 @@ class SamlIdentityManager implements IdentityManager 'title' => 'urn:oid:2.5.4.12', ]; - public function hasIdentity(): bool + public function hasRemoteIdentity(): bool { return ! empty($this->getIdentity()); } + /** + * @deprecated Use hasRemoteIdentity() instead + */ + public function hasIdentity(): bool + { + return $this->hasRemoteIdentity(); + } + public function getIdentity(): ?RemoteIdentity { /** @var RemoteIdentity|null $remoteIdentity */ diff --git a/src/Managers/ShibIdentityManager.php b/src/Managers/ShibIdentityManager.php index 54e5798..629d7d2 100644 --- a/src/Managers/ShibIdentityManager.php +++ b/src/Managers/ShibIdentityManager.php @@ -29,11 +29,19 @@ class ShibIdentityManager implements IdentityManager public const SHIB_LOGOUT_URL = '/Shibboleth.sso/Logout'; - public function hasIdentity(): bool + public function hasRemoteIdentity(): bool { return ! empty($this->getIdentity()); } + /** + * @deprecated Use hasRemoteIdentity() instead + */ + public function hasIdentity(): bool + { + return $this->hasRemoteIdentity(); + } + public function getIdentity(): ?RemoteIdentity { /** @var RemoteIdentity|null $remoteIdentity */ diff --git a/src/Middleware/AppTesters.php b/src/Middleware/AppTesters.php index 4bc4ad4..d630021 100644 --- a/src/Middleware/AppTesters.php +++ b/src/Middleware/AppTesters.php @@ -37,7 +37,7 @@ public function handle(Request $request, Closure $next): Response $appTestersField = config('cu-auth.app_testers_field'); $tester = auth()->user()->$appTestersField ?? ''; } else { - $tester = $this->identityManager->getIdentity()?->uniqueUid() ?: ''; + $tester = $this->identityManager->getIdentity()?->id() ?: ''; } if ($this->app_testers->contains($tester)) { diff --git a/src/Middleware/CUAuth.php b/src/Middleware/CUAuth.php index be617de..c7a8b7c 100644 --- a/src/Middleware/CUAuth.php +++ b/src/Middleware/CUAuth.php @@ -36,7 +36,8 @@ public function handle(Request $request, Closure $next): Response return $next($request); } - if (! $this->identityManager->hasIdentity()) { + // If we don't have a remote identity, redirect to the SSO login route. + if (! $this->identityManager->hasRemoteIdentity()) { return redirect()->route('cu-auth.sso-login', ['redirect_url' => $request->fullUrl()]); } diff --git a/tests/Feature/ShibIdentityManagerTest.php b/tests/Feature/ShibIdentityManagerTest.php index 0702449..d01b25b 100644 --- a/tests/Feature/ShibIdentityManagerTest.php +++ b/tests/Feature/ShibIdentityManagerTest.php @@ -102,7 +102,7 @@ public function testCanFailAuthorizing() $this->addCUAuthenticatedListener(authorized: false); $request = $this->getApacheAuthRequest('new-user'); $identityManager = $this->createStub(ShibIdentityManager::class); - $identityManager->method('hasIdentity')->willReturn(true); + $identityManager->method('hasRemoteIdentity')->willReturn(true); $response = (new CUAuth($identityManager)) ->handle($request, fn () => response('OK')); @@ -213,7 +213,7 @@ public function testRouteIsProtectedWithoutUserLookup() $request = $this->getApacheAuthRequest('new-user'); $identityManager = $this->createStub(ShibIdentityManager::class); - $identityManager->method('hasIdentity')->willReturn(true); + $identityManager->method('hasRemoteIdentity')->willReturn(true); $response = (new CUAuth($identityManager))->handle($request, fn () => response('OK')); $this->assertTrue($response->isOk()); From 52ed1652b015872e059f283978d6d59e8f015911 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Wed, 20 May 2026 18:32:17 -0400 Subject: [PATCH 10/22] Add ChecksLocalLogin trait and update LivewireAuth, CUAuth to support local login logic --- src/Middleware/CUAuth.php | 6 +- src/Middleware/Concerns/ChecksLocalLogin.php | 11 +++ src/Middleware/LivewireAuth.php | 26 ++++--- tests/Feature/LivewireAuthTest.php | 79 ++++++++++++++++++++ 4 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/Middleware/Concerns/ChecksLocalLogin.php create mode 100644 tests/Feature/LivewireAuthTest.php diff --git a/src/Middleware/CUAuth.php b/src/Middleware/CUAuth.php index c7a8b7c..27b612b 100644 --- a/src/Middleware/CUAuth.php +++ b/src/Middleware/CUAuth.php @@ -5,11 +5,14 @@ use Closure; use CornellCustomDev\LaravelStarterKit\CUAuth\Events\CUAuthenticated; use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\IdentityManager; +use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\Concerns\ChecksLocalLogin; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class CUAuth { + use ChecksLocalLogin; + public function __construct( protected IdentityManager $identityManager ) {} @@ -17,7 +20,7 @@ public function __construct( public function handle(Request $request, Closure $next): Response { // If local login is allowed and someone is authenticated, let them through. - if (config('cu-auth.allow_local_login') && auth()->check()) { + if ($this->isLoggedInLocally()) { return $next($request); } @@ -26,6 +29,7 @@ public function handle(Request $request, Closure $next): Response return redirect()->secure($request->getRequestUri()); } + // SSO routes need to pass through so the identity manager can process the SSO responses. $passThrough = in_array($request->path(), [ route('cu-auth.sso-login'), route('cu-auth.sso-logout'), diff --git a/src/Middleware/Concerns/ChecksLocalLogin.php b/src/Middleware/Concerns/ChecksLocalLogin.php new file mode 100644 index 0000000..30ea3cf --- /dev/null +++ b/src/Middleware/Concerns/ChecksLocalLogin.php @@ -0,0 +1,11 @@ +check(); + } +} diff --git a/src/Middleware/LivewireAuth.php b/src/Middleware/LivewireAuth.php index 4db53e3..a1589b9 100644 --- a/src/Middleware/LivewireAuth.php +++ b/src/Middleware/LivewireAuth.php @@ -4,27 +4,35 @@ use Closure; use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\IdentityManager; +use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\Concerns\ChecksLocalLogin; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class LivewireAuth { + use ChecksLocalLogin; + public function __construct( protected IdentityManager $identityManager ) {} + /** + * Assure all Livewire updates are from authenticated users. + */ public function handle(Request $request, Closure $next): Response { - // If this is /livewire/update without a logged in user, return forbidden - if ($request->isMethod('POST') && $request->path() === 'livewire/update') { - if (! $this->identityManager->hasIdentity()) { - if (app()->runningInConsole()) { - return response('Forbidden', Response::HTTP_FORBIDDEN); - } - abort(403); - } + if ($request->path() !== 'livewire/update' || $request->getMethod() !== 'POST') { + return $next($request); + } + + if ($this->isLoggedInLocally() || $this->identityManager->hasRemoteIdentity()) { + return $next($request); } - return $next($request); + // This is a /livewire/update without a logged in user, so return forbidden. + if (app()->runningInConsole()) { + return response('Forbidden', Response::HTTP_FORBIDDEN); + } + abort(403); } } diff --git a/tests/Feature/LivewireAuthTest.php b/tests/Feature/LivewireAuthTest.php new file mode 100644 index 0000000..6f423c9 --- /dev/null +++ b/tests/Feature/LivewireAuthTest.php @@ -0,0 +1,79 @@ +setLaravelSession(app('session.store')); + + return $request; + } + + public function testAllowsRequestWithRemoteIdentity() + { + $identityManager = $this->createStub(ShibIdentityManager::class); + $identityManager->method('hasRemoteIdentity')->willReturn(true); + + $response = (new LivewireAuth($identityManager)) + ->handle($this->getLivewireUpdateRequest(), fn () => response('OK')); + + $this->assertTrue($response->isOk()); + } + + public function testBlocksRequestWithNoIdentity() + { + $identityManager = $this->createStub(ShibIdentityManager::class); + $identityManager->method('hasRemoteIdentity')->willReturn(false); + + $response = (new LivewireAuth($identityManager)) + ->handle($this->getLivewireUpdateRequest(), fn () => response('OK')); + + $this->assertTrue($response->isForbidden()); + } + + public function testAllowsLocallyAuthenticatedUserWhenLocalLoginEnabled() + { + config(['cu-auth.allow_local_login' => true]); + $identityManager = $this->createStub(ShibIdentityManager::class); + $identityManager->method('hasRemoteIdentity')->willReturn(false); + auth()->login($this->getTestUser()); + + $response = (new LivewireAuth($identityManager)) + ->handle($this->getLivewireUpdateRequest(), fn () => response('OK')); + + $this->assertTrue($response->isOk()); + } + + public function testBlocksLocallyAuthenticatedUserWhenLocalLoginDisabled() + { + config(['cu-auth.allow_local_login' => false]); + $identityManager = $this->createStub(ShibIdentityManager::class); + $identityManager->method('hasRemoteIdentity')->willReturn(false); + auth()->login($this->getTestUser()); + + $response = (new LivewireAuth($identityManager)) + ->handle($this->getLivewireUpdateRequest(), fn () => response('OK')); + + $this->assertTrue($response->isForbidden()); + } + + public function testAllowsNonLivewireRoute() + { + $identityManager = $this->createStub(ShibIdentityManager::class); + $identityManager->method('hasRemoteIdentity')->willReturn(false); + $request = Request::create('/some/other/route', 'POST'); + $request->setLaravelSession(app('session.store')); + + $response = (new LivewireAuth($identityManager)) + ->handle($request, fn () => response('OK')); + + $this->assertTrue($response->isOk()); + } +} From 1e2534d00ed96e15ac2c6ef559fb3845070a7ebe Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Wed, 20 May 2026 18:32:26 -0400 Subject: [PATCH 11/22] CLAUDE.md file --- CLAUDE.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..beebddf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Run all tests +composer test + +# Run a single test file +vendor/bin/phpunit tests/Feature/AppTestersTest.php + +# Run a single test by name +vendor/bin/phpunit --filter testMethodName + +# Lint / fix code style +vendor/bin/pint + +# Full CI (prepare + test) +composer ci +``` + +## Architecture + +This is a **Laravel package** (not an application) providing SSO authentication middleware for Cornell University apps. The namespace is `CornellCustomDev\LaravelStarterKit\CUAuth\`. + +### Core abstraction: `IdentityManager` + +`src/Managers/IdentityManager.php` is the central interface. `CUAuthServiceProvider` binds one concrete implementation as a singleton based on `CU_AUTH_IDENTITY_MANAGER`: + +- **`ShibIdentityManager`** (`apache-shib`) — reads Shibboleth attributes from Apache server variables (`$_SERVER`). For local development, falls back to `REMOTE_USER` env var when `APP_ENV != production`. +- **`SamlIdentityManager`** (`php-saml`) — uses the OneLogin PHP-SAML toolkit for SAML SP flows. + +Both return a `RemoteIdentity` data object (readonly class) that normalizes identity data from either IdP. + +### Authentication flow + +1. **`CUAuth` middleware** guards routes. It checks `IdentityManager::hasRemoteIdentity()` and redirects to `cu-auth.sso-login` if not authenticated. If `allow_local_login = true`, a locally-authenticated Laravel user bypasses the SSO check entirely (`isLoggedInLocally()` in the `ChecksLocalLogin` trait, shared with `LivewireAuth`). +2. If `require_local_user = true`, the middleware fires the `CUAuthenticated` event after SSO auth. The consuming app must listen for this event to log in or create a local Laravel user. +3. **`AppTesters` middleware** can be stacked after `CUAuth` to restrict non-production access to users listed in `APP_TESTERS`. + +### Routes registered by the package + +- `GET /sso/login` → `AuthController::login` (redirects to IdP) +- `GET /sso/logout` → `AuthController::logout` (SLO) +- `GET/POST /sso/acs` → `AuthController::acs` (SAML assertion consumer / Shib return; CSRF-exempt) +- `GET /sso/metadata` → `AuthController::metadata` (SAML SP metadata) + +### `RemoteIdentity` key methods + +- `id()` — NetID or CWID (unique within the IdP) +- `principalName()` — `eduPersonPrincipalName` (e.g., `netid@cornell.edu`); unique across Cornell and Weill IdPs +- `email()` — returns `principalName` if set, else alias mail +- `uniqueUid()` — **deprecated**; use `principalName()` for cross-IdP uniqueness + +### Livewire support + +Setting `REQUIRE_LIVEWIRE_AUTH=true` makes the service provider override Livewire's update route to require `LivewireAuth` middleware, blocking unauthenticated POSTs to `/livewire/update`. + +### Testing + +Tests use Orchestra Testbench. `FeatureTestCase` sets up an in-memory SQLite database and loads Laravel's default migrations. Unit tests extend `UnitTestCase`. The package has no database migrations of its own. From 55d10349c773c0aadbe304d83f739f7a033e5fa0 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Wed, 20 May 2026 18:44:44 -0400 Subject: [PATCH 12/22] Consolidate requireLivewireAuth() out of CUAuthServiceProvider --- src/CUAuthServiceProvider.php | 8 +------- src/Middleware/LivewireAuth.php | 16 ++++++++++++---- tests/Feature/LivewireAuthTest.php | 13 ------------- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/CUAuthServiceProvider.php b/src/CUAuthServiceProvider.php index 15b6923..22152c7 100644 --- a/src/CUAuthServiceProvider.php +++ b/src/CUAuthServiceProvider.php @@ -55,13 +55,7 @@ public function boot(): void $this->loadRoutesFrom(__DIR__.'/../routes/cu-auth.php'); if (config('cu-auth.require_livewire_auth')) { - if (class_exists('Livewire')) { - \Livewire::setUpdateRoute(function ($handle) { - // Only logged in users can post data to livewire components - return Route::post('/livewire/update', $handle) - ->middleware(['web', LivewireAuth::class]); - }); - } + LivewireAuth::requireLivewireAuth(); } } } diff --git a/src/Middleware/LivewireAuth.php b/src/Middleware/LivewireAuth.php index a1589b9..20f9125 100644 --- a/src/Middleware/LivewireAuth.php +++ b/src/Middleware/LivewireAuth.php @@ -6,6 +6,7 @@ use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\IdentityManager; use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\Concerns\ChecksLocalLogin; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Route; use Symfony\Component\HttpFoundation\Response; class LivewireAuth @@ -16,15 +17,22 @@ public function __construct( protected IdentityManager $identityManager ) {} + public static function requireLivewireAuth(): void + { + if (class_exists('Livewire\Livewire')) { + \Livewire\Livewire::setUpdateRoute(function ($handle) { + // Only logged in users can post data to livewire components + return Route::post('/livewire/update', $handle) + ->middleware(['web', LivewireAuth::class]); + }); + } + } + /** * Assure all Livewire updates are from authenticated users. */ public function handle(Request $request, Closure $next): Response { - if ($request->path() !== 'livewire/update' || $request->getMethod() !== 'POST') { - return $next($request); - } - if ($this->isLoggedInLocally() || $this->identityManager->hasRemoteIdentity()) { return $next($request); } diff --git a/tests/Feature/LivewireAuthTest.php b/tests/Feature/LivewireAuthTest.php index 6f423c9..fb9203d 100644 --- a/tests/Feature/LivewireAuthTest.php +++ b/tests/Feature/LivewireAuthTest.php @@ -63,17 +63,4 @@ public function testBlocksLocallyAuthenticatedUserWhenLocalLoginDisabled() $this->assertTrue($response->isForbidden()); } - - public function testAllowsNonLivewireRoute() - { - $identityManager = $this->createStub(ShibIdentityManager::class); - $identityManager->method('hasRemoteIdentity')->willReturn(false); - $request = Request::create('/some/other/route', 'POST'); - $request->setLaravelSession(app('session.store')); - - $response = (new LivewireAuth($identityManager)) - ->handle($request, fn () => response('OK')); - - $this->assertTrue($response->isOk()); - } } From 4e9827544e16eff6dd7fb8059f2c3b573066ec72 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Wed, 20 May 2026 19:06:57 -0400 Subject: [PATCH 13/22] Linting --- src/CUAuthServiceProvider.php | 1 - src/Middleware/LivewireAuth.php | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CUAuthServiceProvider.php b/src/CUAuthServiceProvider.php index 22152c7..200fd97 100644 --- a/src/CUAuthServiceProvider.php +++ b/src/CUAuthServiceProvider.php @@ -6,7 +6,6 @@ use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\SamlIdentityManager; use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\ShibIdentityManager; use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\LivewireAuth; -use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; class CUAuthServiceProvider extends ServiceProvider diff --git a/src/Middleware/LivewireAuth.php b/src/Middleware/LivewireAuth.php index 20f9125..019edb1 100644 --- a/src/Middleware/LivewireAuth.php +++ b/src/Middleware/LivewireAuth.php @@ -7,6 +7,7 @@ use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\Concerns\ChecksLocalLogin; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; +use Livewire\Livewire; use Symfony\Component\HttpFoundation\Response; class LivewireAuth @@ -20,7 +21,7 @@ public function __construct( public static function requireLivewireAuth(): void { if (class_exists('Livewire\Livewire')) { - \Livewire\Livewire::setUpdateRoute(function ($handle) { + Livewire::setUpdateRoute(function ($handle) { // Only logged in users can post data to livewire components return Route::post('/livewire/update', $handle) ->middleware(['web', LivewireAuth::class]); From ee85a6ddb6bcad92ec1c2012ced096a296172dd2 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Wed, 20 May 2026 19:14:31 -0400 Subject: [PATCH 14/22] Better documentation for hasIdentity() deprecation --- src/Managers/IdentityManager.php | 5 +++++ src/Managers/SamlIdentityManager.php | 4 +--- src/Managers/ShibIdentityManager.php | 4 +--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Managers/IdentityManager.php b/src/Managers/IdentityManager.php index 8beb7e4..146fe88 100644 --- a/src/Managers/IdentityManager.php +++ b/src/Managers/IdentityManager.php @@ -9,6 +9,11 @@ interface IdentityManager { public function hasRemoteIdentity(): bool; + /** + * @deprecated Use hasRemoteIdentity() instead. + */ + public function hasIdentity(): bool; + public function getIdentity(): ?RemoteIdentity; public function storeIdentity(): ?RemoteIdentity; diff --git a/src/Managers/SamlIdentityManager.php b/src/Managers/SamlIdentityManager.php index 4284877..6acbc07 100644 --- a/src/Managers/SamlIdentityManager.php +++ b/src/Managers/SamlIdentityManager.php @@ -62,9 +62,7 @@ public function hasRemoteIdentity(): bool return ! empty($this->getIdentity()); } - /** - * @deprecated Use hasRemoteIdentity() instead - */ + /** {@inheritDoc} */ public function hasIdentity(): bool { return $this->hasRemoteIdentity(); diff --git a/src/Managers/ShibIdentityManager.php b/src/Managers/ShibIdentityManager.php index 629d7d2..71e05ae 100644 --- a/src/Managers/ShibIdentityManager.php +++ b/src/Managers/ShibIdentityManager.php @@ -34,9 +34,7 @@ public function hasRemoteIdentity(): bool return ! empty($this->getIdentity()); } - /** - * @deprecated Use hasRemoteIdentity() instead - */ + /** {@inheritDoc} */ public function hasIdentity(): bool { return $this->hasRemoteIdentity(); From 28f5be129a5312231452a0a3b5bbec9ea9093a2b Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Mon, 1 Jun 2026 12:55:10 -0400 Subject: [PATCH 15/22] Clarify RemoteIdentity properties --- src/DataObjects/RemoteIdentity.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/DataObjects/RemoteIdentity.php b/src/DataObjects/RemoteIdentity.php index fab9bed..b0bcbda 100644 --- a/src/DataObjects/RemoteIdentity.php +++ b/src/DataObjects/RemoteIdentity.php @@ -28,12 +28,13 @@ public static function fromData( return new RemoteIdentity( idp: $idp, uid: $uid, - principalName: $eduPersonPrincipalName - ?? trim($mail ?? ''), + principalName: trim($eduPersonPrincipalName ?? ''), displayName: $displayName ?? $cn ?? trim(($givenName ?? '').' '.($sn ?? '')), - email: trim($mail ?? ''), + email: $eduPersonPrincipalName + ?? $mail + ?? '', mail: trim($mail ?? ''), data: $data, ); @@ -69,12 +70,22 @@ public function principalName(): string return $this->principalName; } + public function primaryEmail(): string + { + return $this->principalName; + } + + public function emailAlias(): ?string + { + return $this->mail; + } + /** * Returns the primary email (netid@cornell.edu|cwid@med.cornell.edu) if available, otherwise the alias email. */ public function email(): string { - return $this->principalName ?: $this->mail; + return $this->email; } /** From 478e99734c1620288d8e17ff4d948a0ccddb6871 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Mon, 1 Jun 2026 13:09:48 -0400 Subject: [PATCH 16/22] Address PR feedback --- .github/workflows/laravel-pint.yml | 2 +- composer.json | 7 +------ src/DataObjects/RemoteIdentity.php | 16 ++++------------ 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/.github/workflows/laravel-pint.yml b/.github/workflows/laravel-pint.yml index adf6336..0ae57f3 100644 --- a/.github/workflows/laravel-pint.yml +++ b/.github/workflows/laravel-pint.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run Laravel Pint uses: aglipanci/laravel-pint-action@latest diff --git a/composer.json b/composer.json index a546530..b135f4a 100644 --- a/composer.json +++ b/composer.json @@ -31,12 +31,7 @@ ], "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", "prepare": "@php vendor/bin/testbench package:discover --ansi", - "test": "@php vendor/bin/phpunit -c ./ --color", - "sync": "@php bin/sync", - "ci": [ - "@post-autoload-dump", - "@test" - ] + "test": "@php vendor/bin/phpunit -c ./ --color" }, "config": { "sort-packages": true diff --git a/src/DataObjects/RemoteIdentity.php b/src/DataObjects/RemoteIdentity.php index b0bcbda..d114170 100644 --- a/src/DataObjects/RemoteIdentity.php +++ b/src/DataObjects/RemoteIdentity.php @@ -32,9 +32,9 @@ public static function fromData( displayName: $displayName ?? $cn ?? trim(($givenName ?? '').' '.($sn ?? '')), - email: $eduPersonPrincipalName - ?? $mail - ?? '', + email: trim($eduPersonPrincipalName ?? '') + ?: trim($mail ?? '') + ?: '', mail: trim($mail ?? ''), data: $data, ); @@ -62,20 +62,12 @@ public function uniqueUid(): string }; } - /* - * Returns the eduPersonPrincipalName - */ - public function principalName(): string - { - return $this->principalName; - } - public function primaryEmail(): string { return $this->principalName; } - public function emailAlias(): ?string + public function emailAlias(): string { return $this->mail; } From 4933a9ad0d2a7e71f2340d4a066c784098d4f336 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Mon, 1 Jun 2026 13:15:25 -0400 Subject: [PATCH 17/22] Security fix version for php-saml --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b135f4a..27e1c0f 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": "^8.3", "ext-openssl": "*", "illuminate/support": "^12.0|^13.0", - "onelogin/php-saml": "^4.3.1" + "onelogin/php-saml": "^4.3.2" }, "require-dev": { "laravel/pint": "^1.20", From f79e013e23b9a578907758b94d9bb01f5cda7d8b Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Mon, 1 Jun 2026 13:27:29 -0400 Subject: [PATCH 18/22] Deprecate RemoteIdentity->email() --- src/DataObjects/RemoteIdentity.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/DataObjects/RemoteIdentity.php b/src/DataObjects/RemoteIdentity.php index d114170..64bbb70 100644 --- a/src/DataObjects/RemoteIdentity.php +++ b/src/DataObjects/RemoteIdentity.php @@ -32,9 +32,6 @@ public static function fromData( displayName: $displayName ?? $cn ?? trim(($givenName ?? '').' '.($sn ?? '')), - email: trim($eduPersonPrincipalName ?? '') - ?: trim($mail ?? '') - ?: '', mail: trim($mail ?? ''), data: $data, ); @@ -62,6 +59,11 @@ public function uniqueUid(): string }; } + public function principalName(): string + { + return $this->principalName; + } + public function primaryEmail(): string { return $this->principalName; @@ -74,10 +76,12 @@ public function emailAlias(): string /** * Returns the primary email (netid@cornell.edu|cwid@med.cornell.edu) if available, otherwise the alias email. + * + * @deprecated Use of email() should be replaced with primaryEmai() or emailAlias(), as appropriate. */ public function email(): string { - return $this->email; + return $this->primaryEmail() ?: $this->emailAlias(); } /** From 517087d03cee7aaada1c6b95c079fc89878fb839 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Mon, 1 Jun 2026 13:45:49 -0400 Subject: [PATCH 19/22] Deprecate RemoteIdentity->email() --- src/Listeners/AuthorizeUser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Listeners/AuthorizeUser.php b/src/Listeners/AuthorizeUser.php index a088e49..164df51 100644 --- a/src/Listeners/AuthorizeUser.php +++ b/src/Listeners/AuthorizeUser.php @@ -20,13 +20,13 @@ public function handle(CUAuthenticated $event, ?RemoteIdentity $remoteIdentity = // Look for a matching user. $userModel = config('auth.providers.users.model'); - $user = $userModel::firstWhere('email', $remoteIdentity->email()); + $user = $userModel::firstWhere('email', $remoteIdentity->primaryEmail()); if (empty($user)) { // User does not exist, so create them. $user = new $userModel; $user->name = $remoteIdentity->name(); - $user->email = $remoteIdentity->email(); + $user->email = $remoteIdentity->primaryEmail(); $user->password = Str::random(32); $user->save(); Log::info("AuthorizeUser: Created user $user->email with ID $user->id."); From 4b6776a12ddb7340d26d738d5741d59a22c9d11f Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Mon, 1 Jun 2026 13:55:27 -0400 Subject: [PATCH 20/22] AuthorizeUser example manages email alias --- src/Listeners/AuthorizeUser.php | 4 ++-- tests/Feature/SamlIdentityManagerTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Listeners/AuthorizeUser.php b/src/Listeners/AuthorizeUser.php index 164df51..33f4198 100644 --- a/src/Listeners/AuthorizeUser.php +++ b/src/Listeners/AuthorizeUser.php @@ -20,13 +20,13 @@ public function handle(CUAuthenticated $event, ?RemoteIdentity $remoteIdentity = // Look for a matching user. $userModel = config('auth.providers.users.model'); - $user = $userModel::firstWhere('email', $remoteIdentity->primaryEmail()); + $user = $userModel::firstWhere('email', $remoteIdentity->primaryEmail() ?: $remoteIdentity->emailAlias()); if (empty($user)) { // User does not exist, so create them. $user = new $userModel; $user->name = $remoteIdentity->name(); - $user->email = $remoteIdentity->primaryEmail(); + $user->email = $remoteIdentity->primaryEmail() ?: $remoteIdentity->emailAlias(); $user->password = Str::random(32); $user->save(); Log::info("AuthorizeUser: Created user $user->email with ID $user->id."); diff --git a/tests/Feature/SamlIdentityManagerTest.php b/tests/Feature/SamlIdentityManagerTest.php index 29f3135..533c4eb 100644 --- a/tests/Feature/SamlIdentityManagerTest.php +++ b/tests/Feature/SamlIdentityManagerTest.php @@ -147,7 +147,7 @@ public function testAuthorizeUser() $remoteIdentity = $identityManager->retrieveIdentity([ 'uid' => ['netid'], 'displayName' => ['Test User'], - 'mail' => ['cwid@med.cornell.edu'], + 'eduPersonPrincipalName' => ['cwid@med.cornell.edu'], ]); $event = new CUAuthenticated('netid@cornell.edu'); $listener = new AuthorizeUser($identityManager); From 09133e896707762067f3797b5dcd858e4e09a03f Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Mon, 1 Jun 2026 14:21:43 -0400 Subject: [PATCH 21/22] Deprecate RemoteIdentity->email() --- src/Listeners/AuthorizeUser.php | 4 ++-- tests/Feature/SamlIdentityManagerTest.php | 2 +- tests/Feature/ShibIdentityManagerTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Listeners/AuthorizeUser.php b/src/Listeners/AuthorizeUser.php index a088e49..164df51 100644 --- a/src/Listeners/AuthorizeUser.php +++ b/src/Listeners/AuthorizeUser.php @@ -20,13 +20,13 @@ public function handle(CUAuthenticated $event, ?RemoteIdentity $remoteIdentity = // Look for a matching user. $userModel = config('auth.providers.users.model'); - $user = $userModel::firstWhere('email', $remoteIdentity->email()); + $user = $userModel::firstWhere('email', $remoteIdentity->primaryEmail()); if (empty($user)) { // User does not exist, so create them. $user = new $userModel; $user->name = $remoteIdentity->name(); - $user->email = $remoteIdentity->email(); + $user->email = $remoteIdentity->primaryEmail(); $user->password = Str::random(32); $user->save(); Log::info("AuthorizeUser: Created user $user->email with ID $user->id."); diff --git a/tests/Feature/SamlIdentityManagerTest.php b/tests/Feature/SamlIdentityManagerTest.php index 29f3135..533c4eb 100644 --- a/tests/Feature/SamlIdentityManagerTest.php +++ b/tests/Feature/SamlIdentityManagerTest.php @@ -147,7 +147,7 @@ public function testAuthorizeUser() $remoteIdentity = $identityManager->retrieveIdentity([ 'uid' => ['netid'], 'displayName' => ['Test User'], - 'mail' => ['cwid@med.cornell.edu'], + 'eduPersonPrincipalName' => ['cwid@med.cornell.edu'], ]); $event = new CUAuthenticated('netid@cornell.edu'); $listener = new AuthorizeUser($identityManager); diff --git a/tests/Feature/ShibIdentityManagerTest.php b/tests/Feature/ShibIdentityManagerTest.php index 0702449..304a4ef 100644 --- a/tests/Feature/ShibIdentityManagerTest.php +++ b/tests/Feature/ShibIdentityManagerTest.php @@ -286,7 +286,7 @@ public function testAuthorizeUser() 'Shib_Identity_Provider' => 'https://shibidp-test.cit.cornell.edu/idp/shibboleth', 'uid' => 'netid', 'displayName' => 'Test User', - 'mail' => 'netid@cornell.edu', + 'eduPersonPrincipalName' => 'netid@cornell.edu', ]); $event = new CUAuthenticated('netid@cornell.edu'); $listener = new AuthorizeUser($identityManager); From 22561fcab02bff86b506bf7dd59773f7dcf9a388 Mon Sep 17 00:00:00 2001 From: Eric Woods Date: Mon, 1 Jun 2026 14:45:32 -0400 Subject: [PATCH 22/22] README for LivewireAuth --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 3cc46fa..9142066 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Middleware for authorizing Laravel users. - Apache mod_shib integration - [AppTesters](#apptesters) - Limit access to users in the `APP_TESTERS` environment variable - [Local Login](#local-login) - Allow Laravel users to log in with a local username and password +- [Livewire Auth](#livewire-auth) - Block unauthenticated Livewire update requests ## Use Cases @@ -163,4 +164,16 @@ For testing purposes, the environment variable "ALLOW_LOCAL_LOGIN" can be set to ```dotenv # File: .env ALLOW_LOCAL_LOGIN=true +``` + + +## Livewire Auth +Blocks unauthenticated POST requests to `/livewire/update`, preventing anonymous users from interacting with Livewire components. + +### Usage + +```dotenv +# File: .env +REQUIRE_LIVEWIRE_AUTH=true +```