diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1f5431a --- /dev/null +++ b/composer.json @@ -0,0 +1,39 @@ +{ + "name": "uisits/laravel-shibboleth", + "description": "Enable basic Shibboleth support for Laravel 6.x", + "authors": [ + { + "name": "Chinwal Prasad", + "email": "pchin3@uis.edu" + } + ], + "require": { + "illuminate/support": "5.* || ^6.0", + "mrclay/shibalike": "1.0.0", + "laravel/framework": "^5.4 || ^6.0", + "tymon/jwt-auth": "1.0.x-dev" + }, + "autoload": { + "psr-4": { + "StudentAffairsUwm\\Shibboleth\\": "src/StudentAffairsUwm/Shibboleth" + } + }, + "autoload-dev": { + "psr-4": { + "StudentAffairsUwm\\Shibboleth\\Tests\\Stubs\\": "tests/setup/Stubs", + "App\\": "tests/setup/app" + } + }, + "minimum-stability": "stable", + "require-dev": { + "phpunit/phpunit": "^6.0", + "orchestra/testbench": "3.4.*" + }, + "extra": { + "laravel": { + "providers": [ + "StudentAffairsUwm\\Shibboleth\\ShibbolethServiceProvider" + ] + } + } +} diff --git a/src/.htaccess b/src/.htaccess new file mode 100644 index 0000000..05994c3 --- /dev/null +++ b/src/.htaccess @@ -0,0 +1,31 @@ + + + Options -MultiViews + + + RewriteEngine On + + # Redirect Trailing Slashes... + RewriteRule ^(.*)/$ /$1 [L,R=301] + + # Handle Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + + + + AuthType shibboleth + Require shibboleth + ShibUseHeaders On + ShibRequireSession Off + ShibRequestSetting isPassive Off + + + + AuthType shibboleth + Require shibboleth + ShibUseHeaders On + ShibRequireSession Off + ShibRequestSetting isPassive Off + diff --git a/src/StudentAffairsUwm/Shibboleth/Controllers/ShibbolethController.php b/src/StudentAffairsUwm/Shibboleth/Controllers/ShibbolethController.php new file mode 100644 index 0000000..f47151e --- /dev/null +++ b/src/StudentAffairsUwm/Shibboleth/Controllers/ShibbolethController.php @@ -0,0 +1,251 @@ +config = new \Shibalike\Config(); + $this->config->idpUrl = '/emulated/idp'; + + $stateManager = $this->getStateManager(); + + $this->sp = new \Shibalike\SP($stateManager, $this->config); + $this->sp->initLazySession(); + + $this->idp = new \Shibalike\IdP($stateManager, $this->getAttrStore(), $this->config); + } + + $this->user = $user; + } + + /** + * Create the session, send the user away to the IDP + * for authentication. + */ + public function login() + { + if (config('shibboleth.emulate_idp') === true) { + return Redirect::to(action('\\' . __class__ . '@emulateLogin') + . '?target=' . action('\\' . __class__ . '@idpAuthenticate')); + } + + return Redirect::to('https://' . Request::server('SERVER_NAME') + . ':' . Request::server('SERVER_PORT') . config('shibboleth.idp_login') + . '?target=' . action('\\' . __class__ . '@idpAuthenticate')); + } + + /** + * Setup authentication based on returned server variables + * from the IdP. + */ + public function idpAuthenticate() + { + if (empty(config('shibboleth.user'))) { + throw new \Exception('No user attribute mapping for server variables.'); + } + + foreach (config('shibboleth.user') as $local => $server) { + $map[$local] = $this->getServerVariable($server); + } + + if (empty($map['email'])) { + return abort(403, 'Unauthorized'); + } + + $userClass = config('auth.providers.users.model', 'App\User'); + + // Attempt to login with the email, if success, update the user model + // with data from the Shibboleth headers (if present) + if (Auth::attempt(array('email' => $map['email']), true)) { + $user = $userClass::where('email', '=', $map['email'])->first(); + + // Update the model as necessary + $user->update($map); + } + + // Add user and send through auth. + elseif (config('shibboleth.add_new_users', true)) { + $map['password'] = 'shibboleth'; + $user = $userClass::create($map); + Auth::login($user); + } else { + return abort(403, 'Unauthorized'); + } + + Session::regenerate(); + + $route = config('shibboleth.authenticated'); + + if (config('jwtauth') === true) { + $route .= $this->tokenizeRedirect($user, ['auth_type' => 'idp']); + } + + return redirect()->intended($route); + } + + /** + * Destroy the current session and log the user out, redirect them to the main route. + */ + public function destroy() + { + Auth::logout(); + Session::flush(); + + if (config('jwtauth')) { + $token = JWTAuth::parseToken(); + $token->invalidate(); + } + + if (config('shibboleth.emulate_idp') == true) { + return Redirect::to(action('\\' . __class__ . '@emulateLogout')); + } + + return Redirect::to('https://' . Request::server('SERVER_NAME') . config('shibboleth.idp_logout')); + } + + /** + * Emulate a login via Shibalike + */ + public function emulateLogin() + { + $from = (Request::input('target') != null) ? Request::input('target') : $this->getServerVariable('HTTP_REFERER'); + + $this->sp->makeAuthRequest($from); + $this->sp->redirect(); + } + + /** + * Emulate a logout via Shibalike + */ + public function emulateLogout() + { + $this->sp->logout(); + + $referer = $this->getServerVariable('HTTP_REFERER'); + + die("Goodbye, fair user. Return from whence you came!"); + } + + /** + * Emulate the 'authentication' via Shibalike + */ + public function emulateIdp() + { + $data = []; + + if (Request::input('username') != null) { + $username = (Request::input('username') === Request::input('password')) ? + Request::input('username') : ''; + + $userAttrs = $this->idp->fetchAttrs($username); + if ($userAttrs) { + $this->idp->markAsAuthenticated($username); + $this->idp->redirect(route('shibboleth-authenticate')); + } + + $data['error'] = 'Incorrect username and/or password'; + } + + return view('shibalike::IdpLogin', $data); + } + + /** + * Function to get an attribute store for Shibalike + */ + private function getAttrStore() + { + return new \Shibalike\Attr\Store\ArrayStore(config('shibboleth.emulate_idp_users')); + } + + /** + * Gets a state manager for Shibalike + */ + private function getStateManager() + { + $session = \UserlandSession\SessionBuilder::instance() + ->setSavePath(sys_get_temp_dir()) + ->setName('SHIBALIKE_BASIC') + ->build(); + + return new \Shibalike\StateManager\UserlandSession($session); + } + + /** + * Wrapper function for getting server variables. + * Since Shibalike injects $_SERVER variables Laravel + * doesn't pick them up. So depending on if we are + * using the emulated IdP or a real one, we use the + * appropriate function. + */ + private function getServerVariable($variableName) + { + if (config('shibboleth.emulate_idp') == true) { + return isset($_SERVER[$variableName]) ? + $_SERVER[$variableName] : null; + } + + $variable = Request::server($variableName); + + return (!empty($variable)) ? + $variable : Request::server('REDIRECT_' . $variableName); + } + + /* + * Simple function that allows configuration variables + * to be either names of views, or redirect routes. + */ + private function viewOrRedirect($view) + { + return (View::exists($view)) ? view($view) : Redirect::to($view); + } + + /** + * Uses JWTAuth to tokenize the user and returns a URL query string. + * + * @param App\User $user + * @param array $customClaims + * @return string + */ + private function tokenizeRedirect($user, $customClaims) + { + // This is where we used to setup a session. Now we will setup a token. + $token = JWTAuth::fromUser($user, $customClaims); + + // We need to pass the token... how? + // Let's try this. + return "?token=$token"; + } +} diff --git a/src/StudentAffairsUwm/Shibboleth/Entitlement.php b/src/StudentAffairsUwm/Shibboleth/Entitlement.php new file mode 100644 index 0000000..65dffd5 --- /dev/null +++ b/src/StudentAffairsUwm/Shibboleth/Entitlement.php @@ -0,0 +1,32 @@ +model = $model; + } + + /** + * Retrieve a user by their unique identifier. + * + * @param mixed $identifier + * @return \Illuminate\Auth\Authenticatable | null + */ + public function retrieveById($identifier) + { + $user = $this->retrieveByCredentials(['id' => $identifier]); + return ($user && $user->getAuthIdentifier() == $identifier) ? + $user : null; + } + + /** + * Retrieve a user by the given credentials. + * + * @param array $credentials + * @return Illuminate\Auth\Authenticatable | null + */ + public function retrieveByCredentials(array $credentials) + { + if (count($credentials) == 0) { + return null; + } + + $class = '\\' . ltrim($this->model, '\\'); + $user = new $class; + + $query = $user->newQuery(); + foreach ($credentials as $key => $value) { + if (!Str::contains($key, 'password')) { + $query->where($key, $value); + } + } + + return $query->first(); + } + + /** + * Validate a user against the given credentials. + * + * @param \Illuminate\Auth\Authenticatable $user + * @param array $credentials + * @return bool + */ + public function validateCredentials(Authenticatable $user, array $creds) + { + return isset($creds['password']) + ? Hash::check($creds['password'], $user->getAuthPassword()) + : true; + } + + /** + * Update the "remember me" token for the given user in storage. + * + * @param \Illuminate\Auth\Authenticatable $user + * @param string $token + * @return void + */ + public function updateRememberToken(Authenticatable $user, $token) + { + // Not Implemented + } + + /** + * Retrieve a user by by their unique identifier and "remember me" token. + * + * @param mixed $identifier + * @param string $token + * @return \Illuminate\Auth\Authenticatable | null + */ + public function retrieveByToken($identifier, $token) + { + // Not Implemented + } +} diff --git a/src/StudentAffairsUwm/Shibboleth/ShibalikeServiceProvider.php b/src/StudentAffairsUwm/Shibboleth/ShibalikeServiceProvider.php new file mode 100644 index 0000000..8d84cbe --- /dev/null +++ b/src/StudentAffairsUwm/Shibboleth/ShibalikeServiceProvider.php @@ -0,0 +1,25 @@ +loadViewsFrom(__DIR__ . '/../../resources/views/shibalike', 'shibalike'); + + $this->publishes([ + __DIR__ . '/../../resources/views/shibalike/' => resource_path('views/vendor/shibalike'), + ]); + + $this->loadRoutesFrom(__DIR__ . '/../../routes/shibalike.php'); + } +} diff --git a/src/StudentAffairsUwm/Shibboleth/ShibbolethServiceProvider.php b/src/StudentAffairsUwm/Shibboleth/ShibbolethServiceProvider.php new file mode 100644 index 0000000..aae409e --- /dev/null +++ b/src/StudentAffairsUwm/Shibboleth/ShibbolethServiceProvider.php @@ -0,0 +1,42 @@ +publishes([ + __DIR__ . '/../../config/shibboleth.php' => config_path('shibboleth.php'), + ]); + + $this->loadRoutesFrom(__DIR__ . '/../../routes/shibboleth.php'); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + if (config('jwtauth')) { + $this->app->register('Tymon\JWTAuth\Providers\JWTAuthServiceProvider'); + $loader = AliasLoader::getInstance(); + $loader->alias('JWTAuth', 'Tymon\JWTAuth\Facades\JWTAuth'); + $loader->alias('JWTFactory', 'Tymon\JWTAuth\Facades\JWTFactory'); + } + + $this->app['auth']->provider('shibboleth', function ($app) { + return new Providers\ShibbolethUserProvider($app['config']['auth.providers.users.model']); + }); + } +} diff --git a/src/config/shibboleth.php b/src/config/shibboleth.php new file mode 100644 index 0000000..3c70493 --- /dev/null +++ b/src/config/shibboleth.php @@ -0,0 +1,102 @@ + '/Shibboleth.sso/Login', + 'idp_logout' => '/Shibboleth.sso/Logout', + 'authenticated' => '/', + + /* + |-------------------------------------------------------------------------- + | Emulate an IdP + |-------------------------------------------------------------------------- + | + | In case you do not have access to your Shibboleth environment on + | homestead or your own Vagrant box, you can emulate a Shibboleth + | environment with the help of Shibalike. + | + | The password is the same as the username. + | + | Do not use this in production for literally any reason. + | + */ + + 'emulate_idp' => env('EMULATE_IDP', false), + 'emulate_idp_users' => [ + 'admin' => [ + 'Shib-cn' => 'Admin User', + 'Shib-mail' => 'admin@email.arizona.edu', + 'Shib-givenName' => 'Admin', + 'Shib-sn' => 'User', + 'Shib-emplId' => 'admin', + ], + 'staff' => [ + 'Shib-cn' => 'Staff User', + 'Shib-mail' => 'staff@email.arizona.edu', + 'Shib-givenName' => 'Staff', + 'Shib-sn' => 'User', + 'Shib-emplId' => 'staff', + ], + 'user' => [ + 'Shib-cn' => 'User User', + 'Shib-mail' => 'user@email.arizona.edu', + 'Shib-givenName' => 'User', + 'Shib-sn' => 'User', + 'Shib-emplId' => 'user', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Server Variable Mapping + |-------------------------------------------------------------------------- + | + | Change these to the proper values for your IdP. + | + */ + + 'entitlement' => 'Shib-isMemberOf', + + 'user' => [ + // fillable user model attribute => server variable + 'name' => 'Shib-cn', + 'first_name' => 'Shib-givenName', + 'last_name' => 'Shib-sn', + 'email' => 'Shib-mail', + 'emplid' => 'Shib-emplId', + ], + + /* + |-------------------------------------------------------------------------- + | User Creation and Groups Settings + |-------------------------------------------------------------------------- + | + | Allows you to change if / how new users are added + | + */ + + 'add_new_users' => true, // Should new users be added automatically if they do not exist? + + /* + |-------------------------------------------------------------------------- + | JWT Auth + |-------------------------------------------------------------------------- + | + | JWTs are for the front end to know it's logged in + | + | https://github.com/tymondesigns/jwt-auth + | https://github.com/StudentAffairsUWM/Laravel-Shibboleth-Service-Provider/issues/24 + | + */ + + 'jwtauth' => env('JWTAUTH', false), +]; diff --git a/src/database/migrations/2014_10_12_000000_create_users_table.php b/src/database/migrations/2014_10_12_000000_create_users_table.php new file mode 100644 index 0000000..ca3d0ed --- /dev/null +++ b/src/database/migrations/2014_10_12_000000_create_users_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->string('first_name')->nullable(); + $table->string('last_name')->nullable(); + $table->string('emplid')->nullable(); + $table->rememberToken(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('users'); + } +} diff --git a/src/database/migrations/2014_10_12_100000_create_password_resets_table.php b/src/database/migrations/2014_10_12_100000_create_password_resets_table.php new file mode 100644 index 0000000..d132eaa --- /dev/null +++ b/src/database/migrations/2014_10_12_100000_create_password_resets_table.php @@ -0,0 +1,31 @@ +string('email')->index(); + $table->string('token')->index(); + $table->timestamp('created_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('password_resets'); + } +} diff --git a/src/resources/views/shibalike/IdpLogin.blade.php b/src/resources/views/shibalike/IdpLogin.blade.php new file mode 100644 index 0000000..b876f2f --- /dev/null +++ b/src/resources/views/shibalike/IdpLogin.blade.php @@ -0,0 +1,74 @@ + + + + Emulated IdP Login + + + + +
+

Login to Continue

+
+ + {{ $error ?? "Please login below." }} +

+ + +

+

+ + +

+

+ +

+
+
+ + + diff --git a/src/routes/shibalike.php b/src/routes/shibalike.php new file mode 100644 index 0000000..eec8237 --- /dev/null +++ b/src/routes/shibalike.php @@ -0,0 +1,7 @@ + 'web'], function () { + Route::get('emulated/idp', 'StudentAffairsUwm\Shibboleth\Controllers\ShibbolethController@emulateIdp'); + Route::post('emulated/idp', 'StudentAffairsUwm\Shibboleth\Controllers\ShibbolethController@emulateIdp'); + Route::get('emulated/login', 'StudentAffairsUwm\Shibboleth\Controllers\ShibbolethController@emulateLogin'); + Route::get('emulated/logout', 'StudentAffairsUwm\Shibboleth\Controllers\ShibbolethController@emulateLogout'); +}); diff --git a/src/routes/shibboleth.php b/src/routes/shibboleth.php new file mode 100644 index 0000000..056f39b --- /dev/null +++ b/src/routes/shibboleth.php @@ -0,0 +1,6 @@ + 'web'], function () { + Route::name('shibboleth-login')->get('/shibboleth-login', 'StudentAffairsUwm\Shibboleth\Controllers\ShibbolethController@login'); + Route::name('shibboleth-authenticate')->get('/shibboleth-authenticate', 'StudentAffairsUwm\Shibboleth\Controllers\ShibbolethController@idpAuthenticate'); + Route::name('shibboleth-logout')->get('/shibboleth-logout', 'StudentAffairsUwm\Shibboleth\Controllers\ShibbolethController@destroy'); +});