diff --git a/README.md b/README.md index 7c250a0..fe1a8b1 100644 --- a/README.md +++ b/README.md @@ -77,14 +77,16 @@ For an existing Laravel site, this package can be composer-required to provide t ```shell php artisan starterkit:install ``` - Note: When using as a library or updating an installation, you will not want to install the project files. You may still want to install the theme assets, view components, and possibly example files. Be aware that these will overwrite existing files. + The `starterkit:install` command prompts for a set of install options, so it is safe to run and then make selections. + + Note: When using as a library or updating an installation, you will not want to install the project files. You may still want to install the theme assets, view components, and possibly example files. Be aware that these will overwrite existing files. ## Libraries The libraries included in the Starter Kit are documented in their respective README files: - [Contact/PhoneNumber](src/Contact/README.md): A library for parsing and formatting a phone number. -- [CUAuth](src/CUAuth/README.md): A middleware for authorizing Laravel users, mostly for Apache mod_shib authentication. +- [CUAuth](src/CUAuth/README.md): A middleware for authorizing Laravel users, mostly for single sign-on with Apache mod_shib or SAML PHP Toolkit. ## Deploying a site Once a Media3 site has been created, you have confirmed you can reach the default site via a web browser, and you have access to the site login by command line, the code can be deployed. @@ -92,7 +94,7 @@ Once a Media3 site has been created, you have confirmed you can reach the defaul You will likely need to map the `php` command to the correct version by editing `~/.bashrc` to include this alias (for this to take effect, run `source ~/.bashrc` or just log in again): ```shell # User specific aliases and functions -alias php="/usr/local/bin/ea-php81" +alias php="/usr/local/bin/ea-php83" ``` Since `www/your-site/public` will already exist, you need to do a little moving things around to git clone your site repo from GitHub: diff --git a/composer.json b/composer.json index 04eff51..c300a7d 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "type": "library", "require": { "php": "^8.3", + "cornell-custom-dev/laravel-cu-auth": "^1.1", "cubear/cwd_framework_lite": "^3.0", "giggsey/locale": "^2.8", "illuminate/support": "^11.0|^12.0", @@ -30,8 +31,7 @@ "extra": { "laravel": { "providers": [ - "CornellCustomDev\\LaravelStarterKit\\StarterKitServiceProvider", - "CornellCustomDev\\LaravelStarterKit\\CUAuth\\CUAuthServiceProvider" + "CornellCustomDev\\LaravelStarterKit\\StarterKitServiceProvider" ] } }, diff --git a/config/cu-auth.php b/config/cu-auth.php deleted file mode 100644 index 3164045..0000000 --- a/config/cu-auth.php +++ /dev/null @@ -1,52 +0,0 @@ - to your project .env file to log in as that user. - | - | To require a local user be logged in based on the remote user, set - | REQUIRE_LOCAL_USER to true. - | - */ - 'apache_shib_user_variable' => env('APACHE_SHIB_USER_VARIABLE', 'REMOTE_USER'), - 'remote_user_override' => env('REMOTE_USER'), - - 'require_local_user' => env('REQUIRE_LOCAL_USER', false), - - 'shibboleth_login_url' => env('SHIBBOLETH_LOGIN_URL', '/Shibboleth.sso/Login'), - 'shibboleth_logout_url' => env('SHIBBOLETH_LOGOUT_URL', '/Shibboleth.sso/Logout'), - - /* - |-------------------------------------------------------------------------- - | AppTesters Configuration - |-------------------------------------------------------------------------- - | - | Comma-separated list of users to allow in development environments. - | APP_TESTERS_FIELD is the field on the user model to compare against. - | - */ - 'app_testers' => env('APP_TESTERS', ''), - 'app_testers_field' => env('APP_TESTERS_FIELD', 'netid'), - - /* - |-------------------------------------------------------------------------- - | Allow Local Login - |-------------------------------------------------------------------------- - | - | Allow Laravel password-based login? Typically, this would only be used - | for local or automated testing. - | - */ - 'allow_local_login' => boolval(env('ALLOW_LOCAL_LOGIN', false)), -]; diff --git a/src/CUAuth/CUAuthServiceProvider.php b/src/CUAuth/CUAuthServiceProvider.php deleted file mode 100644 index b6719c7..0000000 --- a/src/CUAuth/CUAuthServiceProvider.php +++ /dev/null @@ -1,29 +0,0 @@ -mergeConfigFrom( - path: __DIR__.'/../../config/cu-auth.php', - key: 'cu-auth', - ); - } - - public function boot(): void - { - if ($this->app->runningInConsole()) { - $this->publishes([ - __DIR__.'/../../config/cu-auth.php' => config_path('cu-auth.php'), - ], StarterKitServiceProvider::PACKAGE_NAME.':'.self::INSTALL_CONFIG_TAG); - } - $this->loadRoutesFrom(__DIR__.'/routes.php'); - } -} diff --git a/src/CUAuth/DataObjects/ShibIdentity.php b/src/CUAuth/DataObjects/ShibIdentity.php deleted file mode 100644 index 0c482dc..0000000 --- a/src/CUAuth/DataObjects/ShibIdentity.php +++ /dev/null @@ -1,110 +0,0 @@ - - 'Shib_Authentication_Instant', // YYYY-MM-DDT00:00:00.000Z - 'Shib_Identity_Provider', // https://shibidp.cit.cornell.edu/idp/shibboleth|https://login.weill.cornell.edu/idp - 'Shib_Session_Expires', // timestamp - 'Shib_Session_Inactivity', // timestamp - 'displayName', // John Doe - 'eduPersonAffiliations', // employee;member;staff - 'eduPersonPrincipalName', // netid@cornell.edu|cwid@med.cornell.edu - 'eduPersonScopedAffiliation', // employee@[med.]cornell.edu;member@[med.]cornell.edu;staff@cornell.edu - 'givenName', // John - 'mail', // alias email - 'sn', // Doe - 'uid', // netid|cwid - ]; - - public function __construct( - public readonly string $idp, - public readonly string $uid, - public readonly string $displayName = '', - public readonly string $email = '', - public readonly array $serverVars = [], - ) {} - - /** - * Shibboleth server variables will be retrieved from the request if not provided. - */ - public static function fromServerVars(?array $serverVars = null): self - { - if (empty($serverVars)) { - $serverVars = app('request')->server(); - } - - return new ShibIdentity( - idp: $serverVars['Shib_Identity_Provider'] ?? '', - uid: $serverVars['uid'] ?? '', - displayName: $serverVars['displayName'] - ?? $serverVars['cn'] - ?? trim(($serverVars['givenName'] ?? '').' '.($serverVars['sn'] ?? '')), - email: $serverVars['eduPersonPrincipalName'] - ?? $serverVars['mail'] ?? '', - serverVars: $serverVars, - ); - } - - public static function getRemoteUser(?Request $request = null): ?string - { - if (empty($request)) { - $request = app('request'); - } - - // If this is a local development environment, allow the local override. - $remote_user_override = self::getRemoteUserOverride(); - - // Apache mod_shib populates the remote user variable if someone is logged in. - return $request->server(config('cu-auth.apache_shib_user_variable')) ?: $remote_user_override; - } - - public static function getRemoteUserOverride(): ?string - { - // If this is a local development environment, allow the local override. - return app()->isLocal() ? config('cu-auth.remote_user_override') : null; - } - - public function isCornellIdP(): bool - { - return str_contains($this->idp, 'cit.cornell.edu'); - } - - public function isWeillIdP(): bool - { - return str_contains($this->idp, 'weill.cornell.edu'); - } - - /** - * Provides a uid that is unique across Cornell IdPs. - */ - public function uniqueUid(): string - { - return match (true) { - $this->isCornellIdP() => $this->uid, - $this->isWeillIdP() => $this->uid.'_w', - }; - } - - /** - * Returns the primary email (netid@cornell.edu|cwid@med.cornell.edu) if available, otherwise the alias email. - */ - public function email(): string - { - return $this->email; - } - - /** - * Returns the display name if available, otherwise the common name, fallback is "givenName sn". - */ - public function name(): string - { - return $this->displayName; - } -} diff --git a/src/CUAuth/Events/CUAuthenticated.php b/src/CUAuth/Events/CUAuthenticated.php deleted file mode 100644 index 28b70e0..0000000 --- a/src/CUAuth/Events/CUAuthenticated.php +++ /dev/null @@ -1,15 +0,0 @@ -query('redirect_uri', '/'); - - if (ShibIdentity::getRemoteUser($request)) { - // Already logged in so redirect to the originally intended URL - return redirect()->to($redirectUri); - } - - // Use the Shibboleth login URL - return redirect(config('cu-auth.shibboleth_login_url').'?target='.urlencode($redirectUri)); - } - - public function shibbolethLogout(Request $request) - { - Auth::logout(); - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - $returnUrl = $request->query('return', '/'); - - if (ShibIdentity::getRemoteUserOverride()) { - // If using locally configured remote user, there is no Shibboleth logout - return redirect()->to($returnUrl); - } - - // Use the Shibboleth logout URL - return redirect(config('cu-auth.shibboleth_logout_url').'?return='.urlencode($returnUrl)); - } -} diff --git a/src/CUAuth/Listeners/AuthorizeUser.php b/src/CUAuth/Listeners/AuthorizeUser.php deleted file mode 100644 index fb7bb2d..0000000 --- a/src/CUAuth/Listeners/AuthorizeUser.php +++ /dev/null @@ -1,33 +0,0 @@ -email()); - - if (empty($user)) { - // User does not exist, so create them. - $user = new $userModel; - $user->name = $shibboleth->name(); - $user->email = $shibboleth->email(); - $user->password = Str::random(32); - $user->save(); - Log::info("AuthorizeUser: Created user $user->email with ID $user->id."); - } - - auth()->login($user); - Log::info("AuthorizeUser: Logged in user $user->email."); - } -} diff --git a/src/CUAuth/Middleware/ApacheShib.php b/src/CUAuth/Middleware/ApacheShib.php deleted file mode 100644 index e680a26..0000000 --- a/src/CUAuth/Middleware/ApacheShib.php +++ /dev/null @@ -1,50 +0,0 @@ -check()) { - return $next($request); - } - - // Shibboleth login route is allowed to pass through. - if ($request->path() == route('cu-auth.shibboleth-login')) { - return $next($request); - } - - // remoteUser will be set for authenticated users. - $remoteUser = ShibIdentity::getRemoteUser($request); - - // Unauthenticated get redirected to Shibboleth login. - if (empty($remoteUser)) { - return redirect()->route('cu-auth.shibboleth-login', [ - 'redirect_uri' => $request->fullUrl(), - ]); - } - - // If requiring a local user, attempt to log in the user. - if (config('cu-auth.require_local_user') && ! auth()->check()) { - event(new CUAuthenticated($remoteUser)); - - // If the authenticated user is still not logged in, return a 403. - if (! auth()->check()) { - if (app()->runningInConsole()) { - return response('Forbidden', Response::HTTP_FORBIDDEN); - } - abort(403); - } - } - - return $next($request); - } -} diff --git a/src/CUAuth/Middleware/AppTesters.php b/src/CUAuth/Middleware/AppTesters.php deleted file mode 100644 index bba3e7b..0000000 --- a/src/CUAuth/Middleware/AppTesters.php +++ /dev/null @@ -1,52 +0,0 @@ -app_testers = Str::of(config('cu-auth.app_testers')) - ->split('/[\s,]+/') - ->filter(); - } - - public function handle(Request $request, Closure $next): Response - { - // Anyone can use production - if (config('app.env') == 'production') { - return $next($request); - } - - // If no app_testers are defined, anyone can use - if ($this->app_testers->isEmpty()) { - return $next($request); - } - - if (auth()->check()) { - $appTestersField = config('cu-auth.app_testers_field'); - $tester = auth()->user()->$appTestersField ?? ''; - } else { - // @TODO Should this be calling a generalized method, not Shibboleth specific? - $tester = ShibIdentity::getRemoteUser($request); - } - - if ($this->app_testers->contains($tester)) { - return $next($request); - } - - if (app()->runningInConsole()) { - return response('Forbidden', Response::HTTP_FORBIDDEN); - } - abort(403); - } -} diff --git a/src/CUAuth/README.md b/src/CUAuth/README.md deleted file mode 100644 index a27b5c6..0000000 --- a/src/CUAuth/README.md +++ /dev/null @@ -1,111 +0,0 @@ -# CUAuth - -Middleware for authorizing Laravel users. - -- [ApacheShib](#apacheshib) - 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 - - -## ApacheShib - -Use with Apache mod_shib to authorize users. - -### Usage - -```php -// File: routes/web.php -use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\ApacheShib; - -Route::group(['middleware' => [ApacheShib::class]], function () { - // Protected routes here - Route::get('profile', [UserController::class, 'show']); -}); -``` - -> _See also: [shibboleth configuration](SHIBBOLETH.md)._ - -#### Simple authentication - -If all pages should be protected and all authenticated users should be authorized, only the ApacheShib middleware is -needed. - -#### User authorization - -If the site should only allow users who are in the database, set `REQUIRE_LOCAL_USER=true` in the `.env` file. - -```dotenv -# File: .env -REQUIRE_LOCAL_USER=true -``` - -Requiring a local user triggers the `CUAuthenticated` event when a user is authenticated via Shibboleth. The site must -[register a listener](https://laravel.com/docs/11.x/events#registering-events-and-listeners) for -the `CUAuthenticated` event. This listener should look up the user in the database and log them in or create a user -as needed. - -> [AuthorizeUser](Listeners/AuthorizeUser.php) is provided as a starting point for handling the CUAuthenticated event. -> It is simplistic and should be replaced with a site-specific implementation in the site code base. It demonstrates -> retrieving user data from [ShibIdentity](DataObjects/ShibIdentity.php) and creating a user if they do not exist. - -### Configuration - -See [config/cu-auth.php](../../config/cu-auth.php) for configuration options, all of which can be set with environment variables. - -To modify the default configuration, publish the configuration file: - -```bash -php artisan vendor:publish --tag=starterkit:cu-auth-config -``` - -### Local testing - -For local testing where mod_shib is not available, the `REMOTE_USER` environment variable can be set to simulate -Shibboleth authentication. Note that `APP_ENV` must be set to "local" for this to work. - -```dotenv -# File: .env -APP_ENV=local -REMOTE_USER=abc123 -``` - -### Notes - -The route `cu-auth.shibboleth-login` (`/shibboleth-login`) is utilized for handling the login process. This -architecture supports sites that do not authenticate all pages and allows Laravel to manage authorization. - -Similarly, the route `cu-auth.shibboleth-logout` (`/shibboleth-logout`) is utilized for handling the logout process. - - -## AppTesters - -Limits non-production access to users in the `APP_TESTERS` environment variable. - -### Usage - -```php -// File: routes/web.php -use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\AppTesters; -use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\ApacheShib; - -Route::group(['middleware' => [ApacheShib::class, AppTesters::class], function () { - Route::get('profile', [UserController::class, 'show']); -}); -``` - -```dotenv -# File: .env -APP_TESTERS=abc123,def456 -``` - -On non-production sites, the [AppTesters](Middleware/AppTesters.php) middleware checks the "APP_TESTERS" environment variable for a comma-separated list of users. If a user is logged in and not in the list, the middleware will return an HTTP_FORBIDDEN response. - -The field used for looking up users is `netid` by default. It is configured in [config/cu-auth.php](../../config/cu-auth.php) file as `app_testers_field`. - - -## Local Login -For testing purposes, the environment variable "ALLOW_LOCAL_LOGIN" can be set to true to bypass the middleware for a currently authenticated user. -```dotenv -# File: .env -ALLOW_LOCAL_LOGIN=true - diff --git a/src/CUAuth/SHIBBOLETH.md b/src/CUAuth/SHIBBOLETH.md deleted file mode 100644 index aeddb69..0000000 --- a/src/CUAuth/SHIBBOLETH.md +++ /dev/null @@ -1,96 +0,0 @@ -# Shibboleth Configuration on Media 3 - -[CUAuth](README.md) is built with consideration of these authentication and authorization components: - -- Shibboleth configuration of identity providers -- Apache configuration of locations protected by Shibboleth -- Laravel app configuration of authorization - - -## Shibboleth ApplicationOverride setup - -In `/etc/shibboleth/shibboleth2.xml`, specify identity providers in the `ApplicationDefaults` element. Use `ApplicationOverride` for alternate IdPs. Make sure to include the `MetadataProvider` for each IdP. - -Key elements (can be set up for any Media 3 server): -```xml -... - - - - - ... - - - - - - ... - - - - ... - - - - - - ... - - - - - - ... - - -``` - -Note: If all sites on a server should use the same IdP, modification of `shibboleth2.xml` is not necessary. - - -## Apache configuration - -To utilize a Shibboleth `ApplicationOverride` configuration, the Apache vhost configuration for the site must specify the -`applicationId`. The vhost configuration is also where it is determined which pages are protected by Shibboleth. - -```apache - - ... - AuthType shibboleth - ShibRequestSetting redirectToSSL 443 - # Laravel will determine when to authenticate - ShibRequestSetting requireSession 0 - # Set the applicationId to use determine the IdP - ShibRequestSetting applicationId idselect-test - require shibboleth - -``` - -Note: If the site uses the default IdP, the `applicationId` is not needed. If all routes for the site should be protected by Shibboleth, `requireSession` can be set to `1`. - -## Laravel configuration - -Use ApacheShib middleware to protect routes that need authorization. - -```php -// File: routes/web.php -use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\ApacheShib; - -Route::get('/', fn () => view('welcome'))->name('welcome'); - -Route::get('login', fn () => Redirect::route('cu-auth.shibboleth-login'))->name('login'); -Route::get('logout', fn () => Redirect::route('cu-auth.shibboleth-logout'))->name('logout'); - -Route::group(['middleware' => [ApacheShib::class, AppTesters::class]], function() { - // Routes that require Shibboleth authentication + any authorization policies -} -``` - -Note: If all pages on the site are protected by Shibboleth, the login and logout routes are not relevant. - -## PHP configuration - -On Media 3 servers, Shibboleth attributes are available only on sites served with suphp. Sites using php-cgi -only pass the validated user identifier. If you need to determine which IdP authenticated the user, you must use suphp. - -> **Important**: Selection of suphp / php-cgi is set server-wide for each PHP version. Additionally, file and directory permissions must be properly set if using suphp. Media 3 support can help with this. diff --git a/src/CUAuth/routes.php b/src/CUAuth/routes.php deleted file mode 100644 index af8989d..0000000 --- a/src/CUAuth/routes.php +++ /dev/null @@ -1,9 +0,0 @@ - ['web']], function () { - Route::get('/shibboleth-login', [AuthController::class, 'shibbolethLogin'])->name('cu-auth.shibboleth-login'); - Route::get('/shibboleth-logout', [AuthController::class, 'shibbolethLogout'])->name('cu-auth.shibboleth-logout'); -}); diff --git a/src/StarterKitServiceProvider.php b/src/StarterKitServiceProvider.php index e4cebe7..cfe3e8a 100644 --- a/src/StarterKitServiceProvider.php +++ b/src/StarterKitServiceProvider.php @@ -2,6 +2,7 @@ namespace CornellCustomDev\LaravelStarterKit; +use CornellCustomDev\LaravelStarterKit\CUAuth\CUAuthServiceProvider; use Illuminate\Support\Facades\File; use Illuminate\Support\Str; use Spatie\LaravelPackageTools\Commands\InstallCommand; @@ -102,6 +103,9 @@ private function install(InstallCommand $command): void 'components' => 'View components (/resources/views/components/cd)', 'examples' => 'Example blade files', 'cu-auth' => 'CUAuth config', + 'php-saml-toolkit' => 'PHP-SAML config', + 'certs' => 'SAML certificates (download IdP cert, generate SP keypair)', + 'certs-weill' => 'SAML certificates for Weill Cornell', ], default: ['files', 'assets', 'components', 'cu-auth'], required: true, @@ -158,7 +162,19 @@ private function install(InstallCommand $command): void } if ($install->contains('cu-auth')) { - $this->publishTag($command, self::PACKAGE_NAME.':'.CuAuth\CuAuthServiceProvider::INSTALL_CONFIG_TAG); + $this->publishTag($command, 'starterkit:'.CUAuthServiceProvider::INSTALL_CONFIG_TAG); + } + + if ($install->contains('php-saml-toolkit')) { + $this->publishTag($command, 'starterkit:'.CuAuth\CuAuthServiceProvider::INSTALL_PHP_SAML_TAG); + } + + if ($install->contains('certs')) { + $command->call('cu-auth:generate-keys', ['--force' => true]); + } + + if ($install->contains('certs-weill')) { + $command->call('cu-auth:generate-keys', ['--force' => true, '--weill' => true]); } info('Installation complete.'); diff --git a/tests/Feature/CUAuth/ApacheShibTest.php b/tests/Feature/CUAuth/ApacheShibTest.php deleted file mode 100644 index a4894b0..0000000 --- a/tests/Feature/CUAuth/ApacheShibTest.php +++ /dev/null @@ -1,272 +0,0 @@ -login($this->getTestUser($event->remoteUser)); - } elseif (auth()->check()) { - auth()->logout(); - } - }); - } - - /** - * Get a request with Apache mod_shib authentication set (or null). - */ - private function getApacheAuthRequest($remote_user = null): Request - { - config(['cu-auth.apache_shib_user_variable' => 'REMOTE_USER_TEST']); - - $request = new Request; - $request->setLaravelSession(app('session.store')); - if ($remote_user !== null) { - $request->server->set('REMOTE_USER_TEST', $remote_user); - } - - return $request; - } - - /** - * Remote user is NOT authenticated by Apache mod_shib. - */ - public function testRedirectsUnauthenticatedRemoteUser() - { - $this->addCUAuthenticatedListener(); - $request = $this->getApacheAuthRequest(); - - $response = (new AuthController)->shibbolethLogin($request); - - $this->assertTrue($response->isRedirect()); - $this->assertStringContainsString(config('cu-auth.shibboleth_login_url'), $response->getTargetUrl()); - } - - /** - * Remote user is authenticated by Apache mod_shib. - */ - public function testAuthenticatesRemoteUser() - { - $this->addCUAuthenticatedListener(); - $request = $this->getApacheAuthRequest('new-user'); - $request->query->set('redirect_uri', '/test'); - - $response = (new AuthController)->shibbolethLogin($request); - - $this->assertTrue($response->isRedirect()); - $this->assertStringContainsString('/test', $response->getTargetUrl()); - } - - public function testLogsOutRemoteUser() - { - $user = $this->getTestUser(); - auth()->login($user); - $request = $this->getApacheAuthRequest($user->email); - $request->query->set('return', '/test'); - - $response = (new AuthController)->shibbolethLogout($request); - - $this->assertTrue($response->isRedirect()); - $this->assertStringContainsString(config('cu-auth.shibboleth_logout_url'), $response->getTargetUrl()); - $this->assertStringContainsString(urlencode('/test'), $response->getTargetUrl()); - } - - /** - * Remote user is authenticated but not authorized. - */ - public function testCanFailAuthorizing() - { - config(['cu-auth.require_local_user' => true]); - $this->addCUAuthenticatedListener(authorized: false); - $request = $this->getApacheAuthRequest('new-user'); - - $response = (new ApacheShib)->handle($request, fn () => response('OK')); - - $this->assertTrue($response->isForbidden()); - } - - /** - * Local user is NOT authenticated. - */ - public function testCanFailAuthenticatingLocalUser() - { - config(['cu-auth.allow_local_login' => true]); - - $response = (new ApacheShib)->handle(new Request, fn () => response('OK')); - - $this->assertTrue($response->isRedirect()); - } - - /** - * Local user is authenticated. - */ - public function testAuthenticatesLocalUser() - { - config(['cu-auth.allow_local_login' => true]); - auth()->login($this->getTestUser()); - - $response = (new ApacheShib)->handle(new Request, fn () => response('OK')); - - $this->assertTrue($response->isOk()); - } - - /** - * Routes for auth testing. - */ - protected function usesAuthRoutes($router): void - { - $router->get('/test/require-cu-auth', fn () => 'OK')->name('test.require-cu-auth') - ->middleware(ApacheShib::class); - $router->get('/test/require-auth', fn () => 'OK')->name('test.require-auth') - ->middleware('auth'); - // Laravel requires a route named "login" in auth login workflow. - $router->get('/test/login', fn () => 'OK')->name('login'); - // Fake shibboleth login url route. - $router->get(config('cu-auth.shibboleth_login_url'), fn () => 'ShibUrl')->name('cu-auth.shibboleth-login'); - } - - #[DefineRoute('usesAuthRoutes')] - public function testRoutesAreProtected() - { - $this->get(route('test'))->assertOk(); - $this->get(route('test.require-auth'))->assertRedirect('/test/login'); - $this->get(route('test.require-cu-auth'))->assertRedirectContains(route('cu-auth.shibboleth-login')); - $this->followingRedirects()->get(route('test.require-cu-auth'))->assertSee('ShibUrl'); - } - - #[DefineRoute('usesAuthRoutes')] - public function testRouteIsProtectedForRemoteUser() - { - $this->addCUAuthenticatedListener(); - config(['cu-auth.apache_shib_user_variable' => 'REMOTE_USER_TEST']); - - // No user is authenticated. - $this->followingRedirects()->get(route('test.require-cu-auth'))->assertSee('ShibUrl'); - - // Remote user is authenticated. - $this->withServerVariables(['REMOTE_USER_TEST' => 'new-user']); - $this->get(route('test.require-cu-auth'))->assertOk(); - } - - #[DefineRoute('usesAuthRoutes')] - public function testRouteIsProtectedForProductionRemoteUser() - { - $this->addCUAuthenticatedListener(); - config(['cu-auth.remote_user_override' => 'new-user']); - - // Override does not work in production environment. - $this->app->detectEnvironment(fn () => 'production'); - $this->followingRedirects()->get(route('test.require-cu-auth'))->assertSee('ShibUrl'); - - // Override works in local environment. - $this->app->detectEnvironment(fn () => 'local'); - $this->get(route('test.require-cu-auth'))->assertOk(); - } - - #[DefineRoute('usesAuthRoutes')] - public function testRouteIsProtectedForLocalUser() - { - config(['cu-auth.apache_shib_user_variable' => 'REMOTE_USER_TEST']); - config(['cu-auth.allow_local_login' => true]); - - // No user is authenticated. - $this->followingRedirects()->get(route('test.require-cu-auth'))->assertSee('ShibUrl'); - - // Local user is authenticated. - $this->actingAs($this->getTestUser()); - $this->get(route('test.require-cu-auth'))->assertOk(); - } - - public function testRouteIsProtectedWithoutUserLookup() - { - config(['cu-auth.remote_user_override' => 'new-user']); - - // Require an authenticated user. - $this->addCUAuthenticatedListener(authorized: false); - $request = $this->getApacheAuthRequest('new-user'); - - $response = (new ApacheShib)->handle($request, fn () => response('OK')); - - $this->assertTrue($response->isOk()); - } - - public function testShibIdentity() - { - $shib = ShibIdentity::fromServerVars([ - 'Shib_Identity_Provider' => 'https://shibidp-test.cit.cornell.edu/idp/shibboleth', - 'uid' => 'netid', - 'mail' => 'netid@cornell.edu', - ]); - - $this->assertTrue($shib->isCornellIdP()); - $this->assertFalse($shib->isWeillIdP()); - $this->assertEquals('netid', $shib->uniqueUid()); - $this->assertEquals('netid@cornell.edu', $shib->email()); - } - - public function testShibWeillIdentity() - { - $shib = ShibIdentity::fromServerVars([ - 'Shib_Identity_Provider' => 'https://login-test.weill.cornell.edu/idp', - 'uid' => 'cwid', - 'mail' => 'cwid@med.cornell.edu', - ]); - - $this->assertFalse($shib->isCornellIdP()); - $this->assertTrue($shib->isWeillIdP()); - $this->assertEquals('cwid_w', $shib->uniqueUid()); - $this->assertEquals('cwid@med.cornell.edu', $shib->email()); - } - - public function testShibNames() - { - $shib = ShibIdentity::fromServerVars([ - 'displayName' => 'Test User', - ]); - $this->assertEquals('Test User', $shib->name()); - - $shib = ShibIdentity::fromServerVars([ - 'cn' => 'Test User', - ]); - $this->assertEquals('Test User', $shib->name()); - - $shib = ShibIdentity::fromServerVars([ - 'givenName' => 'Test', - 'sn' => 'User', - ]); - $this->assertEquals('Test User', $shib->name()); - } - - public function testAuthorizeUser() - { - $serverVars = [ - 'Shib_Identity_Provider' => 'https://shibidp-test.cit.cornell.edu/idp/shibboleth', - 'uid' => 'netid', - 'displayName' => 'Test User', - 'mail' => 'netid@cornell.edu', - ]; - $event = new CUAuthenticated('netid@cornell.edu'); - $listener = new AuthorizeUser; - $listener->handle($event, $serverVars); - - $this->assertTrue(auth()->check()); - $this->assertEquals('Test User', auth()->user()->name); - $this->assertEquals('netid@cornell.edu', auth()->user()->email); - } -} diff --git a/tests/Feature/CUAuth/AppTestersTest.php b/tests/Feature/CUAuth/AppTestersTest.php deleted file mode 100644 index de4d44f..0000000 --- a/tests/Feature/CUAuth/AppTestersTest.php +++ /dev/null @@ -1,79 +0,0 @@ -login($this->getTestUser()); - - Config::set('app.env', 'local'); - Config::set('cu-auth.app_testers', 'test-user'); - $response = (new AppTesters)->handle(new Request, fn () => response('OK')); - $this->assertTrue($response->isForbidden()); - - Config::set('app.env', 'production'); - $response = (new AppTesters)->handle(new Request, fn () => response('OK')); - $this->assertTrue($response->isOk()); - } - - public function testHandleWithAuthorizedUser() - { - Config::set('app.env', 'local'); - Config::set('cu-auth.app_testers_field', 'id'); - Config::set('cu-auth.app_testers', 'a-user, test-user'); - Auth::shouldReceive('check')->andReturn(true); - Auth::shouldReceive('user')->andReturn((object) ['id' => 'test-user']); - - $response = (new AppTesters)->handle(new Request, fn () => response('OK')); - - $this->assertTrue($response->isOk()); - } - - public function testHandleWithUnauthorizedUser() - { - Config::set('app.env', 'local'); - Config::set('cu-auth.app_testers_field', 'id'); - Config::set('cu-auth.app_testers', 'test-user'); - Auth::shouldReceive('check')->andReturn(true); - Auth::shouldReceive('user')->andReturn((object) ['id' => 'not-test-user']); - - $response = (new AppTesters)->handle(new Request, fn () => response('OK')); - - $this->assertTrue($response->isForbidden()); - } - - public function testHandleWithNoUser() - { - Config::set('app.env', 'local'); - Config::set('cu-auth.app_testers_field', 'id'); - Config::set('cu-auth.app_testers', 'test-user'); - Auth::shouldReceive('check')->andReturn(false); - - $response = (new AppTesters)->handle(new Request, fn () => response('OK')); - - // If there is no logged-in user, they should not be able to use the app. - $this->assertTrue($response->isForbidden()); - } - - public function testHandleWithNoAppTesters() - { - Config::set('app.env', 'local'); - Config::set('cu-auth.app_testers_field', 'id'); - Config::set('cu-auth.app_testers', ''); - Auth::shouldReceive('check')->andReturn(true); - Auth::shouldReceive('user')->andReturn((object) ['id' => 'test-user']); - - $response = (new AppTesters)->handle(new Request, fn () => response('OK')); - - // If there are no app testers, anyone can use the app. - $this->assertTrue($response->isOk()); - } -} diff --git a/tests/Feature/InstallStarterKitTest.php b/tests/Feature/InstallStarterKitTest.php index 9dccc68..289227f 100644 --- a/tests/Feature/InstallStarterKitTest.php +++ b/tests/Feature/InstallStarterKitTest.php @@ -2,7 +2,6 @@ namespace CornellCustomDev\LaravelStarterKit\Tests\Feature; -use CornellCustomDev\LaravelStarterKit\CUAuth\CUAuthServiceProvider; use CornellCustomDev\LaravelStarterKit\StarterKitServiceProvider; use CornellCustomDev\LaravelStarterKit\Tests\TestCase; use Illuminate\Support\Facades\File; @@ -50,6 +49,12 @@ public function testCanRunAllInstallations() haystack: File::get("$basePath/resources/views/examples/cd-index.blade.php") ); $this->assertFileExists("$basePath/config/cu-auth.php"); + $this->assertFileExists("$basePath/config/php-saml-toolkit.php"); + $this->assertFileExists("$basePath/storage/app/keys/idp_cert.pem"); + $this->assertStringContainsString( + needle: 'test-weill-idp-cert-contents', + haystack: File::get("$basePath/storage/app/keys/idp_cert.pem") + ); } public function testDeletesInstallFilesBeforeTests() @@ -65,8 +70,12 @@ public function testDeletesInstallFilesBeforeTests() if (Str::endsWith($directory, 'errors')) { continue; } - $this->assertEmpty(File::files($directory)); + $this->assertEmpty(File::files($directory), "$directory should be empty."); } + + $this->assertFileDoesNotExist("$basePath/config/cu-auth.php"); + $this->assertFileDoesNotExist("$basePath/config/php-saml-toolkit.php"); + $this->assertDirectoryDoesNotExist("$basePath/storage/app/keys"); } public function testInstallReplacesFiles() @@ -139,13 +148,15 @@ private function resetInstallFiles(): void File::deleteDirectory("$basePath/resources/views/components"); File::deleteDirectory("$basePath/resources/views/examples"); File::delete("$basePath/config/cu-auth.php"); + File::delete("$basePath/config/php-saml-toolkit.php"); + File::deleteDirectory("$basePath/storage/app/keys"); } private function installAll(string $projectName, string $projectDescription): PendingCommand { return $this->artisan(StarterKitServiceProvider::PACKAGE_NAME.':install') ->expectsQuestion('What would you like to install or update?', [ - 'files', 'assets', 'components', 'examples', 'cu-auth', + 'files', 'assets', 'components', 'examples', 'cu-auth', 'php-saml-toolkit', 'certs', 'certs-weill', ]) ->expectsQuestion('Project name', $projectName) ->expectsQuestion('Project description', $projectDescription) @@ -173,35 +184,4 @@ private function assertContentUpdated(string $projectName, string $projectDescri $landoContents = File::get("$basePath/.lando.yml"); $this->assertStringContainsString(Str::slug($projectName), $landoContents); } - - public function testCanInstallCUAuthConfigFiles() - { - $basePath = $this->applicationBasePath(); - $defaultVariable = 'REMOTE_USER'; - $testVariable = 'REDIRECT_REMOTE_USER'; - // Make sure we have config values - $this->refreshApplication(); - - $userVariable = config('cu-auth.apache_shib_user_variable'); - $this->assertEquals($defaultVariable, $userVariable); - - $this->artisan( - command: 'vendor:publish', - parameters: [ - '--tag' => StarterKitServiceProvider::PACKAGE_NAME.':'.CUAuthServiceProvider::INSTALL_CONFIG_TAG, - '--force' => true, - ]) - ->assertSuccessful(); - - // Update the config file with a test value for cu-auth.apache_shib_user_variable. - File::put("$basePath/config/cu-auth.php", str_replace( - "'$defaultVariable'", - "'$testVariable'", - File::get("$basePath/config/cu-auth.php") - )); - $this->refreshApplication(); - - $userVariable = config('cu-auth.apache_shib_user_variable'); - $this->assertEquals($testVariable, $userVariable); - } }