diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 742ca22..1ddeb9b 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - php-versions: ['8.1', '8.2', '8.3'] + php-versions: ['8.2', '8.3'] steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 265c76d..8ecbb6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /vendor/ -/.phpunit.result.cache +/.phpunit.cache # Logs logs diff --git a/CHANGELOG.md b/CHANGELOG.md index 69f95c9..7ee4a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +## [v11.0.0] - 2024-03-18 +### Changed +- Dropped support for PHP 8.1. +- Support for Laravel 11 has been added + + The route stub has been updated for the new Laravel 11 skeleton. When upgrading, the `withoutMiddleware()` call on the Azure AD callback route must be changed to exclude the `Illuminate\Foundation\Http\Middleware\ValidateCsrfToken` class, since the new Laravel skeleton no longer ships with `App\Http\Middleware\VerifyCsrfToken`. + ## [v10.0.0] - 2024-02-06 ### Added - The `eventhub:dlq:restore-messages` artisan command has been added. This is a tool to move messages from the DLQ back to the original queue for re-processing. diff --git a/composer.json b/composer.json index 2435ff3..7297409 100644 --- a/composer.json +++ b/composer.json @@ -8,12 +8,14 @@ "northwestern university", "laravel" ], - "authors": [{ - "name": "Nicholas Evans", - "email": "nick.evans@northwestern.edu" - }], + "authors": [ + { + "name": "Nicholas Evans", + "email": "nick.evans@northwestern.edu" + } + ], "require": { - "php": ">=8.1", + "php": ">=8.2", "guzzlehttp/guzzle": "^7.0|^6.0", "northwestern-sysdev/event-hub-php-sdk": "^3.0", "laravel/ui": "^3.0|^2.0|^4.0", @@ -22,9 +24,9 @@ "firebase/php-jwt": "^5.3" }, "require-dev": { - "orchestra/testbench": "~8.0", + "orchestra/testbench": "~9.0", "php-coveralls/php-coveralls": "^2.4", - "phpunit/phpunit": "^9.0", + "phpunit/phpunit": "^10.0", "laravel/pint": "^1.13", "larastan/larastan": "^2.0" }, diff --git a/docs/upgrading.md b/docs/upgrading.md index 9bf2d85..d7b6be9 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -1,5 +1,23 @@ # Upgrading +## From v10 to v11 +When upgrading to Laravel 11 from a previous version, if you have applied the Laravel skeleton simplifications, you will need to update the Azure AD callback route when deleting the `\App\Http\Middleware\VerifyCsrfToken`: + +```diff +diff --git a/stubs/routes.stub b/stubs/routes.stub +index 34d4712..c65c785 100644 +--- a/stubs/routes.stub ++++ b/stubs/routes.stub +@@ -5,6 +5,6 @@ Route::get('auth/logout', [\App\Controllers\Auth\WebSSOController::class, 'logou + Route::group(['prefix' => 'auth/azure-ad'], function () { + Route::get('redirect', [\App\Controllers\Auth\WebSSOController::class, 'oauthRedirect'])->name('login-oauth-redirect'); + Route::post('callback', [\App\Controllers\Auth\WebSSOController::class, 'oauthCallback'])->name('login-oauth-callback') +- ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]); ++ ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class]); + Route::post('oauth-logout', [\App\Controllers\Auth\WebSSOController::class, 'oauthLogout'])->name('login-oauth-logout'); + }); +``` + ## From v9 to v10 PHP 7.4 & 8.0 support has been dropped. diff --git a/phpunit.xml b/phpunit.xml index 0efe8a0..02ca2a8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,13 +1,13 @@ - - - - ./src - - + - ./tests + ./tests/Feature + + + ./src + + diff --git a/stubs/routes.stub b/stubs/routes.stub index 34d4712..c65c785 100644 --- a/stubs/routes.stub +++ b/stubs/routes.stub @@ -5,6 +5,6 @@ Route::get('auth/logout', [\App\Controllers\Auth\WebSSOController::class, 'logou Route::group(['prefix' => 'auth/azure-ad'], function () { Route::get('redirect', [\App\Controllers\Auth\WebSSOController::class, 'oauthRedirect'])->name('login-oauth-redirect'); Route::post('callback', [\App\Controllers\Auth\WebSSOController::class, 'oauthCallback'])->name('login-oauth-callback') - ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]); + ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class]); Route::post('oauth-logout', [\App\Controllers\Auth\WebSSOController::class, 'oauthLogout'])->name('login-oauth-logout'); }); \ No newline at end of file diff --git a/tests/Auth/Entity/ActiveDirectoryUserTest.php b/tests/Feature/Auth/Entity/ActiveDirectoryUserTest.php similarity index 85% rename from tests/Auth/Entity/ActiveDirectoryUserTest.php rename to tests/Feature/Auth/Entity/ActiveDirectoryUserTest.php index dc241cc..0127c1e 100644 --- a/tests/Auth/Entity/ActiveDirectoryUserTest.php +++ b/tests/Feature/Auth/Entity/ActiveDirectoryUserTest.php @@ -1,13 +1,13 @@ 'TEST123', diff --git a/tests/Auth/OAuthAuthenticationTest.php b/tests/Feature/Auth/OAuthAuthenticationTest.php similarity index 81% rename from tests/Auth/OAuthAuthenticationTest.php rename to tests/Feature/Auth/OAuthAuthenticationTest.php index cfb88c5..2c3e3dc 100644 --- a/tests/Auth/OAuthAuthenticationTest.php +++ b/tests/Feature/Auth/OAuthAuthenticationTest.php @@ -1,6 +1,6 @@ app['router']->get(__METHOD__, function (Request $request) { return $this->mock_controller()->oauthRedirect($request); @@ -32,7 +32,7 @@ public function test_redirects_to_oauth_provider() $response->assertRedirect(self::OAUTH_DUMMY_PROVIDER_URL); } - public function test_callback_success() + public function test_callback_success(): void { $this->app['router']->get(__METHOD__, function (Request $request) { $oauthUser = $this->createStub(User::class); @@ -52,12 +52,11 @@ public function test_callback_success() $this->assertAuthenticated(); } - /** - * @dataProvider restartableExceptionProvider - */ - public function test_exceptions_restart_flow($exception) + public function test_exceptions_restart_flow_for_invalid_state(): void { + $exception = new InvalidStateException; $this->app['router']->get('/login-oauth-redirect', function () { + // })->name('login-oauth-redirect'); $this->app['router']->get(__METHOD__, function (Request $request) use ($exception) { @@ -71,24 +70,33 @@ public function test_exceptions_restart_flow($exception) $response->assertRedirect('/login-oauth-redirect'); } - public function restartableExceptionProvider() + public function test_exceptions_restart_flow_for_guzzle_400_code(): void { $errorResponse = $this->createStub(ResponseInterface::class); $errorResponse->method('getStatusCode')->willReturn(400); - return [ - 'invalid state' => [new InvalidStateException], - 'guzzle 400 w/ message' => [ - new ClientException( - 'OAuth2 Authorization code was already redeemed', - $this->createStub(RequestInterface::class), - $errorResponse - ), - ], - ]; + $exception = new ClientException( + 'OAuth2 Authorization code was already redeemed', + $this->createStub(RequestInterface::class), + $errorResponse + ); + + $this->app['router']->get('/login-oauth-redirect', function () { + // + })->name('login-oauth-redirect'); + + $this->app['router']->get(__METHOD__, function (Request $request) use ($exception) { + $driver = $this->createStub(AzureDriver::class); + $driver->method('user')->willThrowException($exception); + + return $this->mock_controller($driver)->oauthCallback($request); + }); + + $response = $this->get(__METHOD__); + $response->assertRedirect('/login-oauth-redirect'); } - public function test_unhandled_exceptions_are_rethrown() + public function test_unhandled_exceptions_are_rethrown(): void { $this->app['router']->get(__METHOD__, function (Request $request) { $driver = $this->createStub(AzureDriver::class); @@ -101,7 +109,7 @@ public function test_unhandled_exceptions_are_rethrown() $this->assertEquals('Unhandled, yay!', $response->exception->getMessage()); } - public function test_logout() + public function test_logout(): void { $this->app['router']->post(__METHOD__, function (Request $request) { $driver = $this->createStub(AzureDriver::class); @@ -116,7 +124,7 @@ public function test_logout() $this->assertStringContainsString('/oauth2/v2.0/logout', $response->headers->get('Location')); } - public function test_logout_with_redirect() + public function test_logout_with_redirect(): void { $this->app['router']->post(__METHOD__, function (Request $request) { $driver = $this->createStub(AzureDriver::class); diff --git a/tests/Auth/OpenAM11AuthenticationTest.php b/tests/Feature/Auth/OpenAM11AuthenticationTest.php similarity index 93% rename from tests/Auth/OpenAM11AuthenticationTest.php rename to tests/Feature/Auth/OpenAM11AuthenticationTest.php index 1cdf6b0..e5767ae 100644 --- a/tests/Auth/OpenAM11AuthenticationTest.php +++ b/tests/Feature/Auth/OpenAM11AuthenticationTest.php @@ -1,6 +1,6 @@ set('duo.enabled', false); } // end getEnvironmentSetUp - public function test_successful_login_no_mfa() + public function test_successful_login_no_mfa(): void { $this->app['router']->get(__METHOD__, function (Request $request) { // Doing withCookie() on the get won't work cuz that only injects into the Request, @@ -67,7 +68,7 @@ public function test_successful_login_no_mfa() $this->assertAuthenticated(); } - public function test_redirects_when_no_cookie() + public function test_redirects_when_no_cookie(): void { $this->app['router']->get(__METHOD__, function (Request $request) { unset($_COOKIE['nusso']); @@ -79,7 +80,7 @@ public function test_redirects_when_no_cookie() $this->assertSsoRedirect($response); } - public function test_redirects_when_cookie_is_invalid() + public function test_redirects_when_cookie_is_invalid(): void { $this->app['router']->get(__METHOD__, function (Request $request) { $_COOKIE['nusso'] = 'dummy-token'; @@ -93,7 +94,7 @@ public function test_redirects_when_cookie_is_invalid() $this->assertSsoRedirect($response); } - public function test_exception_when_apigee_key_is_invalid() + public function test_exception_when_apigee_key_is_invalid(): void { $this->app['router']->get(__METHOD__, function (Request $request) { $_COOKIE['nusso'] = 'dummy-token'; @@ -106,7 +107,7 @@ public function test_exception_when_apigee_key_is_invalid() $this->get(__METHOD__)->assertStatus(500); } - public function test_sends_to_mfa() + public function test_sends_to_mfa(): void { $this->app['config']->set('nusoa.sso.authTree', 'ldap-and-duo'); $this->app['config']->set('duo.enabled', true); @@ -127,7 +128,7 @@ public function test_sends_to_mfa() $this->assertGreaterThan(-1, strpos($response->getTargetUrl(), 'authIndexValue=ldap-and-duo'), $error); } - public function tests_exception_when_insecure_connection_used() + public function tests_exception_when_insecure_connection_used(): void { // Disable the "force HTTPS" thing in ::prepareUrlForRequest $this->useSecure = false; diff --git a/tests/DirectoySearchTest.php b/tests/Feature/DirectoySearchTest.php similarity index 84% rename from tests/DirectoySearchTest.php rename to tests/Feature/DirectoySearchTest.php index 97b6d25..7384cb7 100644 --- a/tests/DirectoySearchTest.php +++ b/tests/Feature/DirectoySearchTest.php @@ -1,14 +1,15 @@ api->setHttpClient($this->mockedResponse(200, '{"results":[{ "displayName" : [ "Test E User" ], "givenName" : [ "Test" ], "sn" : [ "User" ], "eduPersonNickname" : [ "test" ], "mail" : "test@example.org", "nuStudentEmail" : "", "title" : [ "Tester" ], "telephoneNumber" : "123 1231234", "nuTelephoneNumber2" : "", "nuTelephoneNumber3" : "", "nuOtherTitle" : "" }]}')); @@ -16,7 +17,7 @@ public function testGoodLookup() $this->assertArrayHasKey('mail', $info); } // end testGoodLookup - public function testBadLookup() + public function testBadLookup(): void { $this->api->setHttpClient($this->mockedResponse(404, '{"errorCode":404,"errorMessage":"No Data Found for = uid=test"}')); @@ -24,7 +25,7 @@ public function testBadLookup() $this->assertNotEmpty($this->api->getLastError()); } // end testBadLookup - public function testBadPerms() + public function testBadPerms(): void { $this->api->setHttpClient($this->mockedResponse(401, '{"fault":{"faultstring":"Invalid ApiKey for given resource","detail":{"errorcode":"oauth.v2.InvalidApiKeyForGivenResource"}}}')); @@ -32,7 +33,7 @@ public function testBadPerms() $this->assertNotEmpty($this->api->getLastError()); } // end testBadPerms - public function testBadApiKey() + public function testBadApiKey(): void { $this->api->setHttpClient($this->mockedResponse(401, '{"fault":{"faultstring":"Failed to resolve API Key variable request.header.apikey","detail":{"errorcode":"steps.oauth.v2.FailedToResolveAPIKey"}}}')); @@ -40,7 +41,7 @@ public function testBadApiKey() $this->assertNotEmpty($this->api->getLastError()); } // end testBadApiKey - public function testConnectionFailure() + public function testConnectionFailure(): void { $this->api->setHttpClient($this->mockedConnError()); diff --git a/tests/Exceptions/ApigeeAuthenticationErrorTest.php b/tests/Feature/Exceptions/ApigeeAuthenticationErrorTest.php similarity index 56% rename from tests/Exceptions/ApigeeAuthenticationErrorTest.php rename to tests/Feature/Exceptions/ApigeeAuthenticationErrorTest.php index 9c5e67c..2753173 100644 --- a/tests/Exceptions/ApigeeAuthenticationErrorTest.php +++ b/tests/Feature/Exceptions/ApigeeAuthenticationErrorTest.php @@ -1,14 +1,15 @@ expectExceptionMessageMatches('/WEBSSO_API_KEY/i'); diff --git a/tests/Exceptions/InsecureSsoErrorTest.php b/tests/Feature/Exceptions/InsecureSsoErrorTest.php similarity index 51% rename from tests/Exceptions/InsecureSsoErrorTest.php rename to tests/Feature/Exceptions/InsecureSsoErrorTest.php index 2047b7d..25a841d 100644 --- a/tests/Exceptions/InsecureSsoErrorTest.php +++ b/tests/Feature/Exceptions/InsecureSsoErrorTest.php @@ -1,14 +1,15 @@ expectExceptionMessageMatches('/https/'); diff --git a/tests/VerifyEventHubHMACTest.php b/tests/Feature/VerifyEventHubHMACTest.php similarity index 89% rename from tests/VerifyEventHubHMACTest.php rename to tests/Feature/VerifyEventHubHMACTest.php index af95520..4fb2fa9 100644 --- a/tests/VerifyEventHubHMACTest.php +++ b/tests/Feature/VerifyEventHubHMACTest.php @@ -1,11 +1,11 @@ header_name = $app['config']->get('nusoa.eventHub.hmacVerificationHeader'); } // end getEnvironmentSetUp - public function test_valid_signature_pass_through() + public function test_valid_signature_pass_through(): void { $success_msg = 'hmac is ok'; $this->app['router']->post(__METHOD__, ['middleware' => self::HMAC_VERIFICATION_MIDDLEWARE, 'uses' => function () use ($success_msg) { @@ -38,7 +38,7 @@ public function test_valid_signature_pass_through() $response->assertSeeText($success_msg); } // end test_valid_signature_pass_through - public function test_invalid_signature_401_unauthorized() + public function test_invalid_signature_401_unauthorized(): void { $this->app['router']->post(__METHOD__, ['middleware' => self::HMAC_VERIFICATION_MIDDLEWARE, 'uses' => function () { return 'middleware passed'; @@ -49,7 +49,7 @@ public function test_invalid_signature_401_unauthorized() $response->assertSeeText('HMAC Validation Failure'); } // end test_invalid_signature_401_unauthorized - public function test_no_header_401_unauthorized() + public function test_no_header_401_unauthorized(): void { $this->app['router']->post(__METHOD__, ['middleware' => self::HMAC_VERIFICATION_MIDDLEWARE, 'uses' => function () { return 'middleware passed'; @@ -60,7 +60,7 @@ public function test_no_header_401_unauthorized() $response->assertSeeText('No HMAC Signature Sent'); } // end test_no_header_401_unauthorized - public function test_bad_hmac_algorithm() + public function test_bad_hmac_algorithm(): void { $this->app['config']->set('nusoa.eventHub.hmacVerificationAlgorithmForPHPHashHmac', 'a very invalid algorithm'); diff --git a/tests/WebSSO/OpenAM11Test.php b/tests/Feature/WebSSO/OpenAM11Test.php similarity index 78% rename from tests/WebSSO/OpenAM11Test.php rename to tests/Feature/WebSSO/OpenAM11Test.php index 6fae5f3..9980cef 100644 --- a/tests/WebSSO/OpenAM11Test.php +++ b/tests/Feature/WebSSO/OpenAM11Test.php @@ -1,6 +1,6 @@ set('duo.enabled', true); } - /** @test */ - public function valid_session() + #[Test] + public function valid_session(): void { $netid = 'netid123'; $this->api->setHttpClient($this->mockedResponse(200, $this->ssoResponseJson($netid))); @@ -39,8 +40,8 @@ public function valid_session() $this->assertEquals($netid, $user->getNetid()); } - /** @test */ - public function invalid_session() + #[Test] + public function invalid_session(): void { $this->api->setHttpClient($this->mockedResponse(407, '')); @@ -48,8 +49,8 @@ public function invalid_session() $this->assertNull($user); } - /** @test */ - public function invalid_apigee_key() + #[Test] + public function invalid_apigee_key(): void { $this->expectException(ApigeeAuthenticationError::class); @@ -58,8 +59,8 @@ public function invalid_apigee_key() $this->api->getUser('test-token'); } - /** @test */ - public function connectivity_error() + #[Test] + public function connectivity_error(): void { $this->api->setHttpClient($this->mockedConnError()); @@ -67,8 +68,8 @@ public function connectivity_error() $this->api->getUser('random'); } - /** @test */ - public function login_url() + #[Test] + public function login_url(): void { $this->assertNotEmpty($this->api->getLoginUrl()); $this->assertNotEmpty($this->api->getLoginUrl('/foobar')); diff --git a/tests/WebhookRouteRegistrationTest.php b/tests/Feature/WebhookRouteRegistrationTest.php similarity index 90% rename from tests/WebhookRouteRegistrationTest.php rename to tests/Feature/WebhookRouteRegistrationTest.php index 0a9256e..391ff55 100644 --- a/tests/WebhookRouteRegistrationTest.php +++ b/tests/Feature/WebhookRouteRegistrationTest.php @@ -1,19 +1,19 @@ router->post('/webhook/foo')->eventHubWebhook('foo.my-queue'); @@ -21,7 +21,7 @@ public function test_route_registration() $this->assertEquals(1, count($registered_hooks)); } // end test_route_registration - public function test_uses_hmac_when_configured() + public function test_uses_hmac_when_configured(): void { $secret = 'abcdefg'; $this->app['config']->set('nusoa.eventHub.hmacVerificationSharedSecret', $secret); @@ -37,7 +37,7 @@ public function test_uses_hmac_when_configured() $this->assertEquals($secret, $hook['webhookSecurity'][0]['secretKey']); } // end test_uses_hmac_when_configured - public function test_custom_security_setup() + public function test_custom_security_setup(): void { $this->app['config']->set('nusoa.eventHub.hmacVerificationSharedSecret', null); @@ -53,7 +53,7 @@ public function test_custom_security_setup() $this->assertEquals($secret, $hook['webhookSecurity'][0]['apiKey']); } // end test_custom_security_setup - public function test_multiple_security_modes() + public function test_multiple_security_modes(): void { $this->app['config']->set('nusoa.eventHub.hmacVerificationSharedSecret', 'hmac-key'); @@ -67,7 +67,7 @@ public function test_multiple_security_modes() $this->assertEquals(2, count($hook['webhookSecurity'])); } // end test_multiple_security_modes - public function test_change_content_type() + public function test_change_content_type(): void { $content_type = 'application/xml'; $route = app()->router->post('/webhook/foo')->eventHubWebhook('foo.my-queue', ['contentType' => $content_type]); diff --git a/tests/TestCase.php b/tests/TestCase.php index 171c63c..5350753 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -17,7 +17,7 @@ abstract class TestCase extends BaseTestCase protected $api; - public function setUp(): void + protected function setUp(): void { parent::setUp(); $this->api = @$this->app->make($this->service);