diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1492202 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.travis.yml b/.travis.yml index 8debc25..65af987 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,31 @@ language: php -php: - - 5.4 - - 5.5 - - 5.6 +env: + global: + - setup=stable +matrix: + fast_finish: true + include: + - php: 7.1 + - php: 7.1 + env: setup=lowest + - php: 7.2 + - php: 7.2 + env: setup=lowest -before_script: +sudo: false + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - phpenv config-rm xdebug.ini || true - travis_retry composer self-update - - travis_retry composer install --prefer-source --no-interaction --dev -script: phpunit +install: + - if [[ $setup = 'stable' ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest; fi + - if [[ $setup = 'lowest' ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-lowest --prefer-stable --no-suggest; fi + +script: vendor/bin/phpunit diff --git a/README.md b/README.md index 2cb6659..1458733 100644 --- a/README.md +++ b/README.md @@ -33,25 +33,13 @@ For older versions of Laravel (<5.5), you have to add the service provider and a ] ``` -Then publish the config file with `php artisan vendor:publish`. This will add the file `app/config/saml2_settings.php`. This config is handled almost directly by [OneLogin](https://github.com/onelogin/php-saml) so you may get further references there, but will cover here what's really necessary. There are some other config about routes you may want to check, they are pretty straightforward. +Then publish the config file with `php artisan vendor:publish`. This will add the file `app/config/saml2.php`. This config is handled almost directly by [OneLogin](https://github.com/onelogin/php-saml) so you may get further references there, but will cover here what's really necessary. There are some other config about routes you may want to check, they are pretty straightforward. ### Configuration -Once you publish your saml2_settings.php to your own files, you need to configure your sp and IDP (remote server). The only real difference between this config and the one that OneLogin uses, is that the SP entityId, assertionConsumerService url and singleLogoutService URL are injected by the library. They are taken from routes 'saml_metadata', 'saml_acs' and 'saml_sls' respectively. - -Remember that you don't need to implement those routes, but you'll need to add them to your IDP configuration. For example, if you use simplesamlphp, add the following to /metadata/sp-remote.php - -```php -$metadata['http://laravel_url/saml2/metadata'] = array( - 'AssertionConsumerService' => 'http://laravel_url/saml2/acs', - 'SingleLogoutService' => 'http://laravel_url/saml2/sls', - //the following two affect what the $Saml2user->getUserId() will return - 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', - 'simplesaml.nameidattribute' => 'uid' -); -``` -You can check that metadata if you actually navigate to 'http://laravel_url/saml2/metadata' +Once you publish your saml2.php to your own files, you need to configure your sp and IDP (remote server). The only real difference between this config and the one that OneLogin uses, is that the SP entityId, assertionConsumerService url and singleLogoutService URL are injected by the library. They are taken from routes 'saml_metadata', 'saml_acs' and 'saml_sls' respectively. +Remember that you don't need to implement those routes, but you'll need to add them to your IDP configuration. ### Usage diff --git a/composer.json b/composer.json index 22dc6b9..792d8fe 100644 --- a/composer.json +++ b/composer.json @@ -12,19 +12,25 @@ } ], "require": { - "php": ">=5.4.0", + "php": "^7.1.3", "ext-openssl": "*", - "illuminate/support": ">=5.0.0", + "illuminate/support": "~5.6", "onelogin/php-saml": "3.0.0.x-dev" }, "require-dev": { - "mockery/mockery": "0.9.*" + "mockery/mockery": "~1.0", + "phpunit/phpunit": "~7.0" }, "autoload": { - "psr-0": { + "psr-4": { "Aacotroneo\\Saml2\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, "extra": { "laravel": { "providers": [ @@ -35,12 +41,6 @@ } } }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/onelogin/php-saml" - } - ], "minimum-stability": "dev", "prefer-stable": true } diff --git a/src/config/saml2_settings.php b/config/saml2.php similarity index 50% rename from src/config/saml2_settings.php rename to config/saml2.php index 042d2b6..a53e9b7 100644 --- a/src/config/saml2_settings.php +++ b/config/saml2.php @@ -1,31 +1,9 @@ true, - - 'routesPrefix' => '/saml2', - - /** - * which middleware group to use for the saml routes - * Laravel 5.2 will need a group which includes StartSession - */ - 'routesMiddleware' => [], +// TODO: this seems questionable at best... +$idp_host = env('SAML2_IDP_HOST'); +return [ /** * Indicates how the parameters will be * retrieved from the sls request for signature validation @@ -42,21 +20,17 @@ */ 'loginRoute' => '/', - /** * Where to redirect after login if no other option was provided */ 'errorRoute' => '/', - - - - /***** - * One Login Settings + /** + * + * OneLogin settings + * */ - - // If 'strict' is True, then the PHP Toolkit will reject unsigned // or unencrypted messages if it expects them signed or encrypted // Also will reject the messages if not strictly follow the SAML @@ -73,8 +47,7 @@ 'proxyVars' => false, // Service Provider Data that we are deploying - 'sp' => array( - + 'sp' => [ // Specifies constraints on the name identifier to be used to // represent the requested subject. // Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported @@ -82,68 +55,37 @@ // Usually x509cert and privateKey of the SP are provided by files placed at // the certs folder. But we can also provide them with the following parameters - 'x509cert' => env('SAML2_SP_x509',''), - 'privateKey' => env('SAML2_SP_PRIVATEKEY',''), - - // Identifier (URI) of the SP entity. - // Leave blank to use the 'saml_metadata' route. - 'entityId' => env('SAML2_SP_ENTITYID',''), - - // Specifies info about where and how the message MUST be - // returned to the requester, in this case our SP. - 'assertionConsumerService' => array( - // URL Location where the from the IdP will be returned, - // using HTTP-POST binding. - // Leave blank to use the 'saml_acs' route - 'url' => '', - ), - // Specifies info about where and how the message MUST be - // returned to the requester, in this case our SP. - // Remove this part to not include any URL Location in the metadata. - 'singleLogoutService' => array( - // URL Location where the from the IdP will be returned, - // using HTTP-Redirect binding. - // Leave blank to use the 'saml_sls' route - 'url' => '', - ), - ), + 'x509cert' => env('SAML2_SP_x509'), + + 'privateKey' => env('SAML2_SP_PRIVATEKEY'), + ], // Identity Provider Data that we want connect with our SP - 'idp' => array( + 'idp' => [ // Identifier of the IdP entity (must be a URI) 'entityId' => env('SAML2_IDP_ENTITYID', $idp_host . '/saml2/idp/metadata.php'), // SSO endpoint info of the IdP. (Authentication Request protocol) - 'singleSignOnService' => array( + 'singleSignOnService' => [ // URL Target of the IdP where the SP will send the Authentication Request Message, // using HTTP-Redirect binding. - 'url' => $idp_host . '/saml2/idp/SSOService.php', - ), + 'url' => env('SAML2_IDP_SSO', $idp_host . '/saml2/idp/SSOService.php'), + ], // SLO endpoint info of the IdP. - 'singleLogoutService' => array( + 'singleLogoutService' => [ // URL Location of the IdP where the SP will send the SLO Request, // using HTTP-Redirect binding. - 'url' => $idp_host . '/saml2/idp/SingleLogoutService.php', - ), + 'url' => env('SAML2_IDP_SLO', $idp_host . '/saml2/idp/SingleLogoutService.php'), + ], // Public x509 certificate of the IdP - 'x509cert' => env('SAML2_IDP_x509', 'MIID/TCCAuWgAwIBAgIJAI4R3WyjjmB1MA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJBUjEVMBMGA1UECAwMQnVlbm9zIEFpcmVzMRUwEwYDVQQHDAxCdWVub3MgQWlyZXMxDDAKBgNVBAoMA1NJVTERMA8GA1UECwwIU2lzdGVtYXMxFDASBgNVBAMMC09yZy5TaXUuQ29tMSAwHgYJKoZIhvcNAQkBFhFhZG1pbmlAc2l1LmVkdS5hcjAeFw0xNDEyMDExNDM2MjVaFw0yNDExMzAxNDM2MjVaMIGUMQswCQYDVQQGEwJBUjEVMBMGA1UECAwMQnVlbm9zIEFpcmVzMRUwEwYDVQQHDAxCdWVub3MgQWlyZXMxDDAKBgNVBAoMA1NJVTERMA8GA1UECwwIU2lzdGVtYXMxFDASBgNVBAMMC09yZy5TaXUuQ29tMSAwHgYJKoZIhvcNAQkBFhFhZG1pbmlAc2l1LmVkdS5hcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbzW/EpEv+qqZzfT1Buwjg9nnNNVrxkCfuR9fQiQw2tSouS5X37W5h7RmchRt54wsm046PDKtbSz1NpZT2GkmHN37yALW2lY7MyVUC7itv9vDAUsFr0EfKIdCKgxCKjrzkZ5ImbNvjxf7eA77PPGJnQ/UwXY7W+cvLkirp0K5uWpDk+nac5W0JXOCFR1BpPUJRbz2jFIEHyChRt7nsJZH6ejzNqK9lABEC76htNy1Ll/D3tUoPaqo8VlKW3N3MZE0DB9O7g65DmZIIlFqkaMH3ALd8adodJtOvqfDU/A6SxuwMfwDYPjoucykGDu1etRZ7dF2gd+W+1Pn7yizPT1q8CAwEAAaNQME4wHQYDVR0OBBYEFPsn8tUHN8XXf23ig5Qro3beP8BuMB8GA1UdIwQYMBaAFPsn8tUHN8XXf23ig5Qro3beP8BuMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGu60odWFiK+DkQekozGnlpNBQz5lQ/bwmOWdktnQj6HYXu43e7sh9oZWArLYHEOyMUekKQAxOK51vbTHzzw66BZU91/nqvaOBfkJyZKGfluHbD0/hfOl/D5kONqI9kyTu4wkLQcYGyuIi75CJs15uA03FSuULQdY/Liv+czS/XYDyvtSLnu43VuAQWN321PQNhuGueIaLJANb2C5qq5ilTBUw6PxY9Z+vtMjAjTJGKEkE/tQs7CvzLPKXX3KTD9lIILmX5yUC3dLgjVKi1KGDqNApYGOMtjr5eoxPQrqDBmyx3flcy0dQTdLXud3UjWVW3N0PYgJtw5yBsS74QTGD4='), - /* - * Instead of use the whole x509cert you can use a fingerprint - * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it) - */ - // 'certFingerprint' => '', - ), - - + 'x509cert' => env('SAML2_IDP_x509'), + ], - /*** + /** * * OneLogin advanced settings * - * */ - // Security settings - 'security' => array( - + 'security' => [ /** signatures and encryptions offered */ // Indicates that the nameID of the sent by this SP @@ -170,7 +112,6 @@ */ 'signMetadata' => false, - /** signatures and encryptions required **/ // Indicates a requirement for the , and @@ -190,36 +131,26 @@ // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), 'requestedAuthnContext' => true, - ), + ], // Contact information template, it is recommended to suply a technical and support contacts - 'contactPerson' => array( - 'technical' => array( + 'contactPerson' => [ + 'technical' => [ 'givenName' => 'name', - 'emailAddress' => 'no@reply.com' - ), - 'support' => array( + 'emailAddress' => 'no@reply.com', + ], + 'support' => [ 'givenName' => 'Support', - 'emailAddress' => 'no@reply.com' - ), - ), + 'emailAddress' => 'no@reply.com', + ], + ], // Organization information template, the info in en_US lang is recomended, add more if required - 'organization' => array( - 'en-US' => array( + 'organization' => [ + 'en-US' => [ 'name' => 'Name', 'displayname' => 'Display Name', - 'url' => 'http://url' - ), - ), - -/* Interoperable SAML 2.0 Web Browser SSO Profile [saml2int] http://saml2int.org/profile/current - - 'authnRequestsSigned' => false, // SP SHOULD NOT sign the , - // MUST NOT assume that the IdP validates the sign - 'wantAssertionsSigned' => true, - 'wantAssertionsEncrypted' => true, // MUST be enabled if SSL/HTTPs is disabled - 'wantNameIdEncrypted' => false, -*/ - -); + 'url' => 'http://url', + ], + ], +]; diff --git a/phpunit.xml b/phpunit.xml index 3347b75..794c7ec 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,12 +7,15 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" - syntaxCheck="false" -> + stopOnFailure="false"> - - ./tests/ + + ./tests + + + ./src + + diff --git a/public/.gitkeep b/public/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..1965a56 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,9 @@ +name('saml2.logout'); +Route::get('/login', 'Saml2Controller@login')->name('saml2.login'); +Route::get('/metadata', 'Saml2Controller@metadata')->name('saml2.metadata'); +Route::post('/acs', 'Saml2Controller@acs')->name('saml2.acs'); +Route::get('/sls', 'Saml2Controller@sls')->name('saml2.sls'); diff --git a/src/Aacotroneo/Saml2/Saml2ServiceProvider.php b/src/Aacotroneo/Saml2/Saml2ServiceProvider.php deleted file mode 100644 index 287c0af..0000000 --- a/src/Aacotroneo/Saml2/Saml2ServiceProvider.php +++ /dev/null @@ -1,119 +0,0 @@ -publishes([ - __DIR__.'/../../config/saml2_settings.php' => config_path('saml2_settings.php'), - ]); - - if (config('saml2_settings.proxyVars', false)) { - \OneLogin\Saml2\Utils::setProxyVars(true); - } - } - - /** - * Register the service provider. - * - * @return void - */ - public function register() - { - $this->registerOneLoginInContainer(); - - $this->app->singleton('Aacotroneo\Saml2\Saml2Auth', function ($app) { - - return new \Aacotroneo\Saml2\Saml2Auth($app['OneLogin_Saml2_Auth']); - }); - - } - - protected function registerOneLoginInContainer() - { - $this->app->singleton('OneLogin_Saml2_Auth', function ($app) { - $config = config('saml2_settings'); - if (empty($config['sp']['entityId'])) { - $config['sp']['entityId'] = URL::route('saml_metadata'); - } - if (empty($config['sp']['assertionConsumerService']['url'])) { - $config['sp']['assertionConsumerService']['url'] = URL::route('saml_acs'); - } - if (!empty($config['sp']['singleLogoutService']) && - empty($config['sp']['singleLogoutService']['url'])) { - $config['sp']['singleLogoutService']['url'] = URL::route('saml_sls'); - } - if (strpos($config['sp']['privateKey'], 'file://')===0) { - $config['sp']['privateKey'] = $this->extractPkeyFromFile($config['sp']['privateKey']); - } - if (strpos($config['sp']['x509cert'], 'file://')===0) { - $config['sp']['x509cert'] = $this->extractCertFromFile($config['sp']['x509cert']); - } - if (strpos($config['idp']['x509cert'], 'file://')===0) { - $config['idp']['x509cert'] = $this->extractCertFromFile($config['idp']['x509cert']); - } - - return new Auth($config); - }); - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return array(); - } - - protected function extractPkeyFromFile($path) { - $res = openssl_get_privatekey($path); - if (empty($res)) { - throw new \Exception('Could not read private key-file at path \'' . $path . '\''); - } - openssl_pkey_export($res, $pkey); - openssl_pkey_free($res); - return $this->extractOpensslString($pkey, 'PRIVATE KEY'); - } - - protected function extractCertFromFile($path) { - $res = openssl_x509_read(file_get_contents($path)); - if (empty($res)) { - throw new \Exception('Could not read X509 certificate-file at path \'' . $path . '\''); - } - openssl_x509_export($res, $cert); - openssl_x509_free($res); - return $this->extractOpensslString($cert, 'CERTIFICATE'); - } - - protected function extractOpensslString($keyString, $delimiter) { - $keyString = str_replace(["\r", "\n"], "", $keyString); - $regex = '/-{5}BEGIN(?:\s|\w)+' . $delimiter . '-{5}\s*(.+?)\s*-{5}END(?:\s|\w)+' . $delimiter . '-{5}/m'; - preg_match($regex, $keyString, $matches); - return empty($matches[1]) ? '' : $matches[1]; - } -} diff --git a/src/Aacotroneo/Saml2/Events/Saml2LoginEvent.php b/src/Events/Saml2LoginEvent.php similarity index 100% rename from src/Aacotroneo/Saml2/Events/Saml2LoginEvent.php rename to src/Events/Saml2LoginEvent.php diff --git a/src/Aacotroneo/Saml2/Events/Saml2LogoutEvent.php b/src/Events/Saml2LogoutEvent.php similarity index 95% rename from src/Aacotroneo/Saml2/Events/Saml2LogoutEvent.php rename to src/Events/Saml2LogoutEvent.php index ae94c93..0f0343f 100644 --- a/src/Aacotroneo/Saml2/Events/Saml2LogoutEvent.php +++ b/src/Events/Saml2LogoutEvent.php @@ -4,7 +4,4 @@ class Saml2LogoutEvent { - - - } diff --git a/src/ExtractOpenssl.php b/src/ExtractOpenssl.php new file mode 100644 index 0000000..291df3c --- /dev/null +++ b/src/ExtractOpenssl.php @@ -0,0 +1,71 @@ +saml2Auth = $saml2Auth; + $this->auth = $auth; } + /** + * This initiates a login request + * + * @return \Illuminate\Http\Response + */ + public function login() + { + $this->auth->login(config('saml2.loginRoute')); + + // TODO: create a laravel redirect + // return redirect()->away($loginUrl); + } + + /** + * This initiates a logout request across all the SSO infrastructure. + * + * @return \Illuminate\Http\Response + */ + public function logout(Request $request) + { + $returnTo = $request->query('returnTo'); + $sessionIndex = $request->query('sessionIndex'); + $nameId = $request->query('nameId'); + // the logout request should end up at the sls route + $this->auth->logout($returnTo, $nameId, $sessionIndex); + + // TODO: create a laravel redirect + // return redirect()->away($logoutUrl); + } /** * Generate local sp metadata + * * @return \Illuminate\Http\Response */ public function metadata() { - - $metadata = $this->saml2Auth->getMetadata(); + $metadata = $this->auth->getMetadata(); return response($metadata, 200, ['Content-Type' => 'text/xml']); } @@ -37,67 +72,49 @@ public function metadata() /** * Process an incoming saml2 assertion request. * Fires 'Saml2LoginEvent' event if a valid user is Found + * + * @return \Illuminate\Http\Response */ public function acs() { - $errors = $this->saml2Auth->acs(); - + $errors = $this->auth->acs(); if (!empty($errors)) { - logger()->error('Saml2 error_detail', ['error' => $this->saml2Auth->getLastErrorReason()]); - session()->flash('saml2_error_detail', [$this->saml2Auth->getLastErrorReason()]); + logger()->error('Saml2 error_detail', ['error' => $this->auth->getLastErrorReason()]); + session()->flash('saml2_error_detail', [$this->auth->getLastErrorReason()]); logger()->error('Saml2 error', $errors); session()->flash('saml2_error', $errors); - return redirect(config('saml2_settings.errorRoute')); + + return redirect(config('saml2.errorRoute')); } - $user = $this->saml2Auth->getSaml2User(); - event(new Saml2LoginEvent($user, $this->saml2Auth)); + $user = $this->auth->getSaml2User(); + + event(new Saml2LoginEvent($user, $this->auth)); $redirectUrl = $user->getIntendedUrl(); if ($redirectUrl !== null) { return redirect($redirectUrl); - } else { - - return redirect(config('saml2_settings.loginRoute')); } + + return redirect(config('saml2.loginRoute')); } /** * Process an incoming saml2 logout request. * Fires 'saml2.logoutRequestReceived' event if its valid. * This means the user logged out of the SSO infrastructure, you 'should' log him out locally too. + * + * @return \Illuminate\Http\Response */ public function sls() { - $error = $this->saml2Auth->sls(config('saml2_settings.retrieveParametersFromServer')); + $error = $this->auth->sls(config('saml2.retrieveParametersFromServer')); if (!empty($error)) { throw new \Exception("Could not log out"); } - return redirect(config('saml2_settings.logoutRoute')); //may be set a configurable default - } - - /** - * This initiates a logout request across all the SSO infrastructure. - */ - public function logout(Request $request) - { - $returnTo = $request->query('returnTo'); - $sessionIndex = $request->query('sessionIndex'); - $nameId = $request->query('nameId'); - $this->saml2Auth->logout($returnTo, $nameId, $sessionIndex); //will actually end up in the sls endpoint - //does not return - } - - - /** - * This initiates a login request - */ - public function login() - { - $this->saml2Auth->login(config('saml2_settings.loginRoute')); + return redirect(config('saml2.logoutRoute')); //may be set a configurable default } - } diff --git a/src/Aacotroneo/Saml2/Saml2Auth.php b/src/Saml2Auth.php similarity index 75% rename from src/Aacotroneo/Saml2/Saml2Auth.php rename to src/Saml2Auth.php index 4724b34..df307d9 100644 --- a/src/Aacotroneo/Saml2/Saml2Auth.php +++ b/src/Saml2Auth.php @@ -5,16 +5,13 @@ use OneLogin\Saml2\Auth; use OneLogin\Saml2\Error; use OneLogin\Saml2\Utils; -use Aacotroneo\Saml2\Events\Saml2LogoutEvent; - -use Log; -use Psr\Log\InvalidArgumentException; +use InvalidArgumentException; class Saml2Auth { /** - * @var \OneLogin_Saml2_Auth + * @var \OneLogin\Saml2\Auth */ protected $auth; @@ -30,13 +27,12 @@ function __construct(Auth $auth) */ function isAuthenticated() { - $auth = $this->auth; - - return $auth->isAuthenticated(); + return $this->auth->isAuthenticated(); } /** * The user info from the assertion + * * @return Saml2User */ function getSaml2User() @@ -46,7 +42,8 @@ function getSaml2User() /** * The ID of the last message processed - * @return String + * + * @return string */ function getLastMessageId() { @@ -66,11 +63,9 @@ function getLastMessageId() * * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters */ - function login($returnTo = null, $parameters = array(), $forceAuthn = false, $isPassive = false, $stay = false, $setNameIdPolicy = true) + function login($returnTo = null, $parameters = [], $forceAuthn = false, $isPassive = false, $stay = false, $setNameIdPolicy = true) { - $auth = $this->auth; - - return $auth->login($returnTo, $parameters, $forceAuthn, $isPassive, $stay, $setNameIdPolicy); + return $this->auth->login($returnTo, $parameters, $forceAuthn, $isPassive, $stay, $setNameIdPolicy); } /** @@ -88,9 +83,7 @@ function login($returnTo = null, $parameters = array(), $forceAuthn = false, $is */ function logout($returnTo = null, $nameId = null, $sessionIndex = null, $nameIdFormat = null) { - $auth = $this->auth; - - $auth->logout($returnTo, [], $nameId, $sessionIndex, false, $nameIdFormat); + $this->auth->logout($returnTo, [], $nameId, $sessionIndex, false, $nameIdFormat); } /** @@ -99,20 +92,16 @@ function logout($returnTo = null, $nameId = null, $sessionIndex = null, $nameIdF */ function acs() { + $this->auth->processResponse(); - /** @var $auth OneLogin_Saml2_Auth */ - $auth = $this->auth; - - $auth->processResponse(); - - $errors = $auth->getErrors(); + $errors = $this->auth->getErrors(); if (!empty($errors)) { return $errors; } - if (!$auth->isAuthenticated()) { - return array('error' => 'Could not authenticate'); + if (!$this->auth->isAuthenticated()) { + return ['error' => 'Could not authenticate']; } return null; @@ -125,17 +114,15 @@ function acs() */ function sls($retrieveParametersFromServer = false) { - $auth = $this->auth; - // destroy the local session by firing the Logout event $keep_local_session = false; $session_callback = function () { - event(new Saml2LogoutEvent()); + event(new Events\Saml2LogoutEvent()); }; - $auth->processSLO($keep_local_session, null, $retrieveParametersFromServer, $session_callback); + $this->auth->processSLO($keep_local_session, null, $retrieveParametersFromServer, $session_callback); - $errors = $auth->getErrors(); + $errors = $this->auth->getErrors(); return $errors; } @@ -147,16 +134,13 @@ function sls($retrieveParametersFromServer = false) */ function getMetadata() { - $auth = $this->auth; - $settings = $auth->getSettings(); + $settings = $this->auth->getSettings(); $metadata = $settings->getSPMetadata(); $errors = $settings->validateMetadata($metadata); if (empty($errors)) { - return $metadata; } else { - throw new InvalidArgumentException( 'Invalid SP metadata: ' . implode(', ', $errors), Error::METADATA_SP_INVALID @@ -165,8 +149,8 @@ function getMetadata() } /** - * Get the last error reason from \OneLogin_Saml2_Auth, useful for error debugging. - * @see \OneLogin_Saml2_Auth::getLastErrorReason() + * Get the last error reason from \OneLogin\Saml2\Auth, useful for error debugging. + * @see \OneLogin\Saml2\Auth::getLastErrorReason() * @return string */ function getLastErrorReason() { diff --git a/src/Saml2ServiceProvider.php b/src/Saml2ServiceProvider.php new file mode 100644 index 0000000..f1bf411 --- /dev/null +++ b/src/Saml2ServiceProvider.php @@ -0,0 +1,104 @@ +registerRoutes(); + + if (config('saml2.proxyVars', false)) { + OneLoginUtils::setProxyVars(true); + } + } + + /** + * Register the Saml2 routes. + * + * @return void + */ + protected function registerRoutes() + { + Route::group([ + 'prefix' => config('saml2.uri', 'saml2'), + 'namespace' => 'Aacotroneo\Saml2\Http\Controllers', + 'middleware' => config('saml2.middleware', 'web'), + ], function () { + $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); + }); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $this->offerPublishing(); + + $this->app->singleton(Auth::class, function ($app) { + return new Auth($this->getAuthConfig()); + }); + + // Should only register a single class in the Service Container probably + $this->app->singleton(Saml2Auth::class, function ($app) { + return new Saml2Auth($app->make(Auth::class)); + }); + + } + + protected function getAuthConfig() + { + // TODO: We probably want to feed our wrapper class the Laravel config + // and then do the manipulation outside of the Service Provider + // (then we can test this behaviour as well +1) + $config = config('saml2'); + + $config['sp']['entityId'] = URL::route('saml2.metadata'); + $config['sp']['assertionConsumerService']['url'] = URL::route('saml2.acs'); + $config['sp']['singleLogoutService']['url'] = URL::route('saml2.sls'); + + // Do we really want to support file:// paths like this? + if (strpos($config['sp']['privateKey'], 'file://') === 0) { + // OneLogin saml will format the key for us, no need to do any of this... + $config['sp']['privateKey'] = ExtractOpenssl::privatekeyFromFile($config['sp']['privateKey']); + } + if (strpos($config['sp']['x509cert'], 'file://') === 0) { + // OneLogin saml will format the cert for us, no need to do any of this... + $config['sp']['x509cert'] = ExtractOpenssl::certFromFile($config['sp']['x509cert']); + } + + if (strpos($config['idp']['x509cert'], 'file://') === 0) { + // OneLogin saml will format the cert for us, no need to do any of this... + $config['idp']['x509cert'] = ExtractOpenssl::certFromFile($config['idp']['x509cert']); + } + + return $config; + } + + /** + * Setup the resource publishing groups for Saml2. + * + * @return void + */ + protected function offerPublishing() + { + if ($this->app->runningInConsole()) { + $this->publishes([ + __DIR__.'/../config/saml2.php' => config_path('saml2.php'), + ], 'saml2-config'); + } + } +} diff --git a/src/Aacotroneo/Saml2/Saml2User.php b/src/Saml2User.php similarity index 83% rename from src/Aacotroneo/Saml2/Saml2User.php rename to src/Saml2User.php index 76e023c..bfbb91c 100644 --- a/src/Aacotroneo/Saml2/Saml2User.php +++ b/src/Saml2User.php @@ -24,10 +24,7 @@ function __construct(Auth $auth) */ function getUserId() { - $auth = $this->auth; - - return $auth->getNameId(); - + return $this->auth->getNameId(); } /** @@ -35,9 +32,7 @@ function getUserId() */ function getAttributes() { - $auth = $this->auth; - - return $auth->getAttributes(); + return $this->auth->getAttributes(); } /** @@ -46,10 +41,9 @@ function getAttributes() * @param string $name The requested attribute of the user. * @return array|null Requested SAML attribute ($name). */ - function getAttribute($name) { - $auth = $this->auth; - - return $auth->getAttribute($name); + function getAttribute($name) + { + return $this->auth->getAttribute($name); } /** @@ -80,10 +74,10 @@ function getIntendedUrl() * @return array|null */ function parseUserAttribute($samlAttribute = null, $propertyName = null) { - if(empty($samlAttribute)) { + if (empty($samlAttribute)) { return null; } - if(empty($propertyName)) { + if (empty($propertyName)) { return $this->getAttribute($samlAttribute); } @@ -95,8 +89,8 @@ function parseUserAttribute($samlAttribute = null, $propertyName = null) { * * @param array $attributes Array of properties which need to be parsed, like this ['email' => 'urn:oid:0.9.2342.19200300.100.1.3'] */ - function parseAttributes($attributes = array()) { - foreach($attributes as $propertyName => $samlAttribute) { + function parseAttributes($attributes = []) { + foreach ($attributes as $propertyName => $samlAttribute) { $this->parseUserAttribute($samlAttribute, $propertyName); } } diff --git a/src/routes.php b/src/routes.php deleted file mode 100644 index 6fe4daa..0000000 --- a/src/routes.php +++ /dev/null @@ -1,33 +0,0 @@ - config('saml2_settings.routesPrefix'), - 'middleware' => config('saml2_settings.routesMiddleware'), -], function () { - - Route::get('/logout', array( - 'as' => 'saml_logout', - 'uses' => 'Aacotroneo\Saml2\Http\Controllers\Saml2Controller@logout', - )); - - Route::get('/login', array( - 'as' => 'saml_login', - 'uses' => 'Aacotroneo\Saml2\Http\Controllers\Saml2Controller@login', - )); - - Route::get('/metadata', array( - 'as' => 'saml_metadata', - 'uses' => 'Aacotroneo\Saml2\Http\Controllers\Saml2Controller@metadata', - )); - - Route::post('/acs', array( - 'as' => 'saml_acs', - 'uses' => 'Aacotroneo\Saml2\Http\Controllers\Saml2Controller@acs', - )); - - Route::get('/sls', array( - 'as' => 'saml_sls', - 'uses' => 'Aacotroneo\Saml2\Http\Controllers\Saml2Controller@sls', - )); -}); diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/ExtractOpensslTest.php b/tests/ExtractOpensslTest.php new file mode 100644 index 0000000..0b74f26 --- /dev/null +++ b/tests/ExtractOpensslTest.php @@ -0,0 +1,30 @@ +assertNotEmpty($extractedCert); + $this->assertEquals($this->expectedCert, $extractedCert); + } + + public function testExtractPrivatekey() + { + $extractedKey = ExtractOpenssl::privatekeyFromFile(__DIR__.'/Fixtures/test.key'); + + $this->assertNotEmpty($extractedKey); + $this->assertEquals($this->expectedKey, $extractedKey); + } +} + \ No newline at end of file diff --git a/tests/Fixtures/test.crt b/tests/Fixtures/test.crt new file mode 100644 index 0000000..458e3d7 --- /dev/null +++ b/tests/Fixtures/test.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC5TCCAc2gAwIBAgIJAObIWyE4hypEMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCXRlc3QudGVzdDAeFw0xODA4MzAxNDQxMzZaFw0xODA5MjkxNDQxMzZaMBQx +EjAQBgNVBAMMCXRlc3QudGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALj5bNdP684tFwBgYgl7Sr0eOaH8NexrQasghTynYm0E/y5voj7gHYQm+hf5 +ZbzKtWFnBugyJfbhMxtx7Df3j/PYjWw5S/I/QWzwzw/d7MEpBqEDy8XPjmnIpqzl +EdeFpbdCTqvguTm+OKJ4JQski34WitRn8OKj+W0COuuU5xqcj84aIlfIojeOuSi8 +FSb8yrASCO1RAUfJHOtpiZ/IUmznKMrUduYP4Fa4jM5WTBPSoZWYMP5yBO9STsR2 +wzWIgeVDAs/xGs7/LjVxGHN76jTKpPy+0SX7aySA49zqRTq8ECm2dX/d+vOXJUu2 +2pNFPKlwSFjJkBbL2VrpqS6KymECAwEAAaM6MDgwFAYDVR0RBA0wC4IJdGVzdC50 +ZXN0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B +AQsFAAOCAQEAtqpKZ5KqymKeLNKs8dcIt5MyhqK5hLDfSLMGZp7MufT+YdsEi+Qw +0bHNwV4d9akt9Horf7L49YzyN+XZs82Wn8Ca1n51++6QWtCu5DpYKQQ1c4R4QbhR +VufJgBIrNhj3fD88R9QB2BjEBsmh+s71rvLB2Ujp3E/PMb2HeM/EXiSCYU5aUJIH +1PBjZ7jNlDLDibiviQz2dMrLmQgzeU/Fai1H4Bh7wGSu/tiIxrwnIQVIFLVJSwFI +7susx5PRfiE/TD8qrz1ZaRx3bwOEBrsR/1c1Rd5SBhHLHXkJj+BLfOhKVRbYMQhH +qMp8zOzBP+o69zrTcSa5O0/xfsPkKulZgw== +-----END CERTIFICATE----- diff --git a/tests/Fixtures/test.key b/tests/Fixtures/test.key new file mode 100644 index 0000000..2aa3531 --- /dev/null +++ b/tests/Fixtures/test.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4+WzXT+vOLRcA +YGIJe0q9Hjmh/DXsa0GrIIU8p2JtBP8ub6I+4B2EJvoX+WW8yrVhZwboMiX24TMb +cew394/z2I1sOUvyP0Fs8M8P3ezBKQahA8vFz45pyKas5RHXhaW3Qk6r4Lk5vjii +eCULJIt+ForUZ/Dio/ltAjrrlOcanI/OGiJXyKI3jrkovBUm/MqwEgjtUQFHyRzr +aYmfyFJs5yjK1HbmD+BWuIzOVkwT0qGVmDD+cgTvUk7EdsM1iIHlQwLP8RrO/y41 +cRhze+o0yqT8vtEl+2skgOPc6kU6vBAptnV/3frzlyVLttqTRTypcEhYyZAWy9la +6akuisphAgMBAAECggEALHhq3mjsfCkC+qgxaa4mjckSegs0u54dr5Kl9asYUrV8 +1CEqlIs1DWyBe/oNp5HkgYJPestzrSL/Mn31GI+AIFPTzE0KITdr91D9twbXwKio +W1WaS/hWeMAwsihwXaxX5vMeDtx8K5G78/OGlGM41ht9TQugMhzR/o8mszSdSxwn +0xHQOC0Agg09G4gDwa/ECwAWdkrXHcGpziXg9Qj5ZLGkbmOJBLOVuWvpbE8oyuij +KBIssxR1AHEr3YbIhcSzfO/IIPGanYcJ/uPXphZxxVvFlmcj29X1rL6LiyL0EOv6 +Re7/7aG8u28Mnjkc4B0dVcSZ0BdD2WmtYh5wP1ToQQKBgQDrPDKrp3m8bn0aNSBo +l5WQxVjhhN/CF78LWR1BCxzn7C9bxOXnM6qNYIyVrgUKuVJ5pvmUbpY+2Ycykzrr +IGLSFPA6y2oPEeXFcAER9WKjYmNto8BsrGqoSPIiZOB7D7wntaC97dCdnCE+RvDe ++pSJ4K9lqkst0WpMQL65upXuhQKBgQDJTXEgWSKgpncV8BSRsPJHdICWgQ2HDuGW +F2eoyQVjptGE4e1mrwGnPR2OW2KGZ8zxIhInO4vVV+tGlnJnP8Lx4hPq5/fgqeZm +I+eh/CXB2m/gjJW4VH2xuJa+tkJYTjlfjRXyYJEg0N4VE6DSkS8VIkeebgFG5hRL +2r1BxJd5LQKBgQCSjK1QrYS09OyxcBmhr5Y5XAk0bnBsXhjiPAFyrTaz8jvK408L +i++cJmNPONvhQ3VzXqgsZfzqaODGjFzvcPy/vtWu+102yEKqj03LX2G1Qi2Jd7QA +wCWuc8uNy+TiJfpljsz2pnsKReOcBdw4Pkpd34HGR6KQh9++Y7Oux+RydQKBgAfS +xam/LRRXQ9uLaBE9cj0KrxCqVU9Bacz+fd3Waio0SoJCkYpjFMpeGq70qECW+iUI +8PGrY8TX1OH6aNnQZZAm/CUt/LkzgSvJC3CFLyZ4ic6NSChQyE3G4bzpsmxiJeKr +xgWUcS94Tpk9GQv17oGAwo3KsqwBtxo3lxFeRZDFAoGBAJC1N8wCNuDRsBlf9ksD +kZDqYwQXBLIASCUCR7Xawq5cjWNL5plv/7h6jA+dIjHHtFQuZZYw4WNs716cCU1T +6il0mUdUoIn2WVsPt7MD91DtNMg/znH8wTw//IeGO3898t4Gk4sltRblalNNC8mC +cedTuDyTcz46fbEGYoJdxguv +-----END PRIVATE KEY----- diff --git a/tests/Saml2/Saml2AuthServiceProviderTest.php b/tests/Saml2AuthServiceProviderTest.php similarity index 70% rename from tests/Saml2/Saml2AuthServiceProviderTest.php rename to tests/Saml2AuthServiceProviderTest.php index ad51d78..6b30666 100644 --- a/tests/Saml2/Saml2AuthServiceProviderTest.php +++ b/tests/Saml2AuthServiceProviderTest.php @@ -1,21 +1,17 @@ assertTrue(true); @@ -23,6 +19,5 @@ public function testSimpleMock() * Cant test here. It uses Laravel dependencies (eg. config()) */ } - } \ No newline at end of file diff --git a/tests/Saml2/Saml2AuthTest.php b/tests/Saml2AuthTest.php similarity index 79% rename from tests/Saml2/Saml2AuthTest.php rename to tests/Saml2AuthTest.php index 0068569..762d41c 100644 --- a/tests/Saml2/Saml2AuthTest.php +++ b/tests/Saml2AuthTest.php @@ -1,38 +1,38 @@ shouldReceive('isAuthenticated')->andReturn('return'); $this->assertEquals('return', $saml2->isAuthenticated()); - } public function testLogin() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); + $auth->shouldReceive('login')->once(); $saml2->login(); + + // TODO: better assertion... + $this->assertTrue(true); } public function testLogout() @@ -41,31 +41,34 @@ public function testLogout() $expectedSessionIndex = 'session_index_value'; $expectedNameId = 'name_id_value'; $expectedNameIdFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'; - $auth = m::mock('OneLogin_Saml2_Auth'); + + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); + $auth->shouldReceive('logout') ->with($expectedReturnTo, [], $expectedNameId, $expectedSessionIndex, false, $expectedNameIdFormat) ->once(); $saml2->logout($expectedReturnTo, $expectedNameId, $expectedSessionIndex, $expectedNameIdFormat); + + // TODO: better assertion... + $this->assertTrue(true); } - public function testAcsError() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); $auth->shouldReceive('processResponse')->once(); - $auth->shouldReceive('getErrors')->once()->andReturn(array('errors')); + $auth->shouldReceive('getErrors')->once()->andReturn(['errors']); $error = $saml2->acs(); $this->assertNotEmpty($error); } - public function testAcsNotAutenticated() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); $auth->shouldReceive('processResponse')->once(); $auth->shouldReceive('getErrors')->once()->andReturn(null); @@ -75,10 +78,9 @@ public function testAcsNotAutenticated() $this->assertNotEmpty($error); } - public function testAcsOK() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); $auth->shouldReceive('processResponse')->once(); $auth->shouldReceive('getErrors')->once()->andReturn(null); @@ -91,7 +93,7 @@ public function testAcsOK() public function testSlsError() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); $auth->shouldReceive('processSLO')->once(); $auth->shouldReceive('getErrors')->once()->andReturn('errors'); @@ -103,7 +105,7 @@ public function testSlsError() public function testSlsOK() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); $auth->shouldReceive('processSLO')->once(); $auth->shouldReceive('getErrors')->once()->andReturn(null); @@ -115,7 +117,7 @@ public function testSlsOK() public function testCanGetLastError() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); $auth->shouldReceive('getLastErrorReason')->andReturn('lastError'); @@ -124,7 +126,7 @@ public function testCanGetLastError() } public function testGetUserAttribute() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); $user = $saml2->getSaml2User(); @@ -137,7 +139,7 @@ public function testGetUserAttribute() { } public function testParseSingleUserAttribute() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); $user = $saml2->getSaml2User(); @@ -152,7 +154,7 @@ public function testParseSingleUserAttribute() { } public function testParseMultipleUserAttributes() { - $auth = m::mock('OneLogin_Saml2_Auth'); + $auth = m::mock(\OneLogin\Saml2\Auth::class); $saml2 = new Saml2Auth($auth); $user = $saml2->getSaml2User(); @@ -176,25 +178,24 @@ public function testParseMultipleUserAttributes() { // $app = m::mock('Illuminate\Contracts\Foundation\Application[register,setDeferredServices]'); // -// $s = m::mock('Aacotroneo\Saml2\Saml2ServiceProvider[publishes]', array($app)); +// $s = m::mock('Aacotroneo\Saml2\Saml2ServiceProvider[publishes]', [$app]); // $s->boot(); // $s->shouldReceive('publishes'); // -// $repo = m::mock('Illuminate\Foundation\ProviderRepository[createProvider,loadManifest,shouldRecompile]', array($app, m::mock('Illuminate\Filesystem\Filesystem'), array(__DIR__.'/services.json'))); -// $repo->shouldReceive('loadManifest')->once()->andReturn(array('eager' => array('foo'), 'deferred' => array('deferred'), 'providers' => array('providers'), 'when' => array())); +// $repo = m::mock('Illuminate\Foundation\ProviderRepository[createProvider,loadManifest,shouldRecompile]', [$app, m::mock('Illuminate\Filesystem\Filesystem'], [__DIR__.'/services.json'])); +// $repo->shouldReceive('loadManifest')->once()->andReturn(['eager' => ['foo'], 'deferred' => ['deferred'], 'providers' => ['providers'], 'when' => []]); // $repo->shouldReceive('shouldRecompile')->once()->andReturn(false); // $provider = m::mock('Illuminate\Support\ServiceProvider'); // $repo->shouldReceive('createProvider')->once()->with('foo')->andReturn($provider); // $app->shouldReceive('register')->once()->with($provider); // $app->shouldReceive('runningInConsole')->andReturn(false); -// $app->shouldReceive('setDeferredServices')->once()->with(array('deferred')); -// $repo->load(array()); +// $app->shouldReceive('setDeferredServices')->once()->with(['deferred']); +// $repo->load([]); // $s = new Saml2ServiceProvider(); // -// $mock = \Mockery::mock(array('pi' => 3.1, 'e' => 2.71)); +// $mock = \Mockery::mock(['pi' => 3.1, 'e' => 2.71]); // $this->assertEquals(3.1416, $mock->pi()); // $this->assertEquals(2.71, $mock->e()); } -