├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── samlidp.php ├── database └── migrations │ └── 2025_01_29_095333_create_saml_service_providers_table.php ├── package-lock.json ├── package.json ├── pint.json ├── resources └── views │ ├── components │ └── input.blade.php │ └── metadata.blade.php ├── routes └── web.php └── src ├── Console ├── CreateCertificate.php └── CreateServiceProvider.php ├── Contracts └── SamlContract.php ├── Events └── Assertion.php ├── Exceptions └── DestinationMissingException.php ├── Http └── Controllers │ ├── LogoutController.php │ └── MetadataController.php ├── Jobs ├── SamlSlo.php └── SamlSso.php ├── LaravelSamlIdpServiceProvider.php ├── Listeners ├── SamlAuthenticated.php ├── SamlLogin.php └── SamlLogout.php ├── Models └── SamlServiceProvider.php └── Traits ├── EventMap.php ├── PerformsSingleSignOn.php └── SamlidpLog.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | max_line_length = 120 11 | quote_type = "single" 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | vendor 4 | .DS_Store 5 | node_modules -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Code Green Creative, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/codegreencreative/laravel-samlidp.svg?style=flat-square)](https://packagist.org/packages/codegreencreative/laravel-samlidp) 2 | [![Total Downloads](https://img.shields.io/packagist/dt/codegreencreative/laravel-samlidp.svg?style=flat-square)](https://packagist.org/packages/codegreencreative/laravel-samlidp) 3 | 4 | [Buy me a coffee](https://www.buymeacoffee.com/upwebdesign) :coffee: 5 | 6 | # Laravel SAML IdP 7 | 8 | This package allows you to implement your own Identification Provider (idP) using the SAML 2.0 standard to be used with supporting SAML 2.0 Service Providers (SP). 9 | 10 | - Starting in version ^5.1, Laravel 9 is supported. 11 | - Starting in version ^5.2.4, Laravel 10 is supported. 12 | - Starting in version ^5.2.9, Laravel 11 is supported. 13 | 14 | In this version we will be allowing for Laravel ^7.0 or ^8.0. 15 | 16 | If you are looking for Laravel ^5.6 see [v1.0](https://github.com/codegreencreative/laravel-samlidp/tree/1.0) 17 | 18 | If you are looking for Laravel ^6.0 use [v2.0](https://github.com/codegreencreative/laravel-samlidp/tree/2.0) 19 | 20 | ## Installation 21 | 22 | ```shell 23 | composer require codegreencreative/laravel-samlidp 24 | ``` 25 | 26 | # Configuration 27 | 28 | ```shell 29 | php artisan vendor:publish --tag="samlidp_config" 30 | ``` 31 | 32 | FileSystem configuration 33 | 34 | ```php 35 | // config/filesystem.php 36 | 37 | 'disks' => [ 38 | 39 | ... 40 | 41 | 'samlidp' => [ 42 | 'driver' => 'local', 43 | 'root' => storage_path() . '/samlidp', 44 | ] 45 | ], 46 | ``` 47 | 48 | Use the following command to create a self signed certificate for your IdP. If you change the certname or keyname to anything other than the default names, you will need to update your `config/samlidp.php` config file to reflect those new file names. 49 | 50 | ```shell 51 | php artisan samlidp:cert [--days --keyname --certname ] 52 | ``` 53 | 54 | ```shell 55 | Options: 56 | --days= Days to add for the expiration date [default: 7800] 57 | --keyname= Name of the certificate key file [default: key.pem] 58 | --certname= Name of the certificate file [default: cert.pem] 59 | ``` 60 | 61 | Optionally, you can set the certificate and key using two environment variables: `SAMLIDP_CERT` and `SAMLIDP_KEY`. 62 | 63 | ## Usage 64 | 65 | Within your login view, probably `resources/views/auth/login.blade.php` add the SAMLRequest directive beneath the CSRF directive: 66 | 67 | ```php 68 | @csrf 69 | @samlidp 70 | ``` 71 | 72 | The SAMLRequest directive will fill out the hidden input automatically when a SAMLRequest is sent by an HTTP request and therefore initiate a SAML authentication attempt. To initiate the SAML auth, the login and redirect processes need to be intervened. This is done using the Laravel events fired upon authentication. 73 | 74 | ## Config 75 | 76 | After you publish the config file, you will need to set up your Service Providers. The key for the Service Provider is a base 64 encoded Consumer Service (ACS) URL. You can get this information from your Service Provider, but you will need to base 64 encode the URL and place it in your config. This is due to config dot notation. 77 | 78 | You may use this command to help generate a new SAML Service Provider: 79 | 80 | ```shell 81 | php artisan samlidp:sp 82 | ``` 83 | 84 | Example SP in `config/samlidp.php` file: 85 | 86 | ```php 87 | 'login', 92 | // The URI to the saml metadata file, this describes your idP 93 | 'issuer_uri' => 'saml/metadata', 94 | // List of all Service Providers 95 | 'sp' => [ 96 | // Base64 encoded ACS URL 97 | 'aHR0cHM6Ly9teWZhY2Vib29rd29ya3BsYWNlLmZhY2Vib29rLmNvbS93b3JrL3NhbWwucGhw' => [ 98 | // ACS URL of the Service Provider 99 | 'destination' => 'https://example.com/saml/acs', 100 | // Simple Logout URL of the Service Provider 101 | 'logout' => 'https://example.com/saml/sls', 102 | // SP certificate 103 | // 'certificate' => '', 104 | // Turn off auto appending of the idp query param 105 | // 'query_params' => false, 106 | // Turn off the encryption of the assertion per SP 107 | // 'encrypt_assertion' => false 108 | ], 109 | ], 110 | // List of guards saml idp will catch Authenticated, Login and Logout events (thanks @abublihi) 111 | 'guards' => ['web'], 112 | ]; 113 | ``` 114 | 115 | #### Setting the service providers to be read from the database 116 | 117 | Run migrations 118 | 119 | ```bash 120 | php artisan migrate 121 | ``` 122 | 123 | Add a service provider to the database table `saml_service_providers`. The database table follows the same principles 124 | as the config file. 125 | 126 | ```php 127 | \CodeGreenCreative\SamlIdp\Models\SamlServiceProvider::class, 132 | // ... 133 | ]; 134 | ``` 135 | 136 | ### Setting the service provider certificate 137 | 138 | There are three options to set the service provider certificate. 139 | 140 | 1. Provide the certificate as a string: 141 | 142 | ```php 143 | [ 148 | // Base64 encoded ACS URL 149 | 'aHR0cHM6Ly9teWZhY2Vib29rd29ya3BsYWNlLmZhY2Vib29rLmNvbS93b3JrL3NhbWwucGhw' => [ 150 | // ... 151 | // SP certificate 152 | // 'certificate' => "-----BEGIN CERTIFICATE-----\nb3BlbnNzaC1rZXktdjEA...LWdlbmVyYXRlZC1rZXkBAgM\n-----END CERTIFICATE-----" 153 | ], 154 | ], 155 | // ... 156 | ]; 157 | ``` 158 | 159 | 2. Load from a variable within the `.env` file. 160 | You can choose an appropriate variable name that best matches your projects requirements. 161 | 162 | ```php 163 | [ 168 | // Base64 encoded ACS URL 169 | 'aHR0cHM6Ly9teWZhY2Vib29rd29ya3BsYWNlLmZhY2Vib29rLmNvbS93b3JrL3NhbWwucGhw' => [ 170 | // ... 171 | // SP certificate 172 | // 'certificate' => env('SAML_SP_CERTIFICATE', '') 173 | ], 174 | ], 175 | // ... 176 | ]; 177 | ``` 178 | 179 | 3. Load the certificate from a file: 180 | 181 | ```php 182 | [ 187 | // Base64 encoded ACS URL 188 | 'aHR0cHM6Ly9teWZhY2Vib29rd29ya3BsYWNlLmZhY2Vib29rLmNvbS93b3JrL3NhbWwucGhw' => [ 189 | // ... 190 | // SP certificate 191 | // 'certificate' => 'file://' . storage_path('samlidp/service-provider.pem') 192 | ], 193 | ], 194 | // ... 195 | ]; 196 | ``` 197 | 198 | ## Log out of IdP after SLO 199 | 200 | If you wish to log out of the IdP after SLO has completed, set `LOGOUT_AFTER_SLO` to `true` in your `.env` perform the logout action on the Idp. 201 | 202 | ``` 203 | // .env 204 | 205 | LOGOUT_AFTER_SLO=true 206 | ``` 207 | 208 | ## Redirect to SLO initiator after logout 209 | 210 | If you wish to return the user back to the SP by which SLO was initiated, you may provide an additional query parameter to the `/saml/logout` route, for example: 211 | 212 | ``` 213 | https://idp.com/saml/logout?return_to=mysp.com 214 | ``` 215 | 216 | After all SP's have been logged out of, the user will be redirected to `mysp.com`. For this to work properly you need to add the `sp_slo_redirects` option to your `config/samlidp.php` config file, for example: 217 | 218 | ```php 219 | [ 227 | 'mysp.com' => 'https://mysp.com', 228 | ], 229 | ]; 230 | ``` 231 | 232 | ## Attributes (optional) 233 | 234 | Service providers may require more additional attributes to be sent via assertion. Its even possible that they require the same information but as a different Claim Type. 235 | 236 | By Default this package will send the following Claim Types: 237 | 238 | `ClaimTypes::EMAIL_ADDRESS` as `auth()->user()->email` 239 | `ClaimTypes::GIVEN_NAME` as `auth()->user()->name` 240 | 241 | This is because Laravel migrations, by default, only supply email and name fields that are usable by SAML 2.0. 242 | 243 | To add additional Claim Types, you can subscribe to the Assertion event: 244 | 245 | `CodeGreenCreative\SamlIdp\Events\Assertion` 246 | 247 | Subscribing to the Event: 248 | 249 | In your `App\Providers\EventServiceProvider` class, add to the already existing `$listen` property... 250 | 251 | ```php 252 | protected $listen = [ 253 | 'App\Events\Event' => [ 254 | 'App\Listeners\EventListener', 255 | ], 256 | 'CodeGreenCreative\SamlIdp\Events\Assertion' => [ 257 | 'App\Listeners\SamlAssertionAttributes' 258 | ] 259 | ]; 260 | ``` 261 | 262 | Sample Listener: 263 | 264 | ```php 265 | attribute_statement 278 | ->addAttribute(new Attribute(ClaimTypes::PPID, auth()->user()->id)) 279 | ->addAttribute(new Attribute(ClaimTypes::NAME, auth()->user()->name)); 280 | } 281 | } 282 | ``` 283 | 284 | ## Digest Algorithm (optional) 285 | 286 | See `\RobRichards\XMLSecLibs\XMLSecurityDSig` for all digest options. 287 | 288 | ```php 289 | \RobRichards\XMLSecLibs\XMLSecurityDSig::SHA1, 294 | ]; 295 | ``` 296 | 297 | [Buy me a coffee](https://www.buymeacoffee.com/upwebdesign) :coffee: 298 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codegreencreative/laravel-samlidp", 3 | "description": "Make your PHP Laravel application an Identification Provider using SAML 2.0. This package allows you to implement your own Identification Provider (idP) using the SAML 2.0 standard to be used with supporting SAML 2.0 Service Providers (SP).", 4 | "keywords": [ 5 | "laravel", 6 | "saml", 7 | "saml 2.0", 8 | "auth", 9 | "acl", 10 | "sso", 11 | "idp" 12 | ], 13 | "license": "MIT", 14 | "require": { 15 | "php": "^7.2.5|^8.0", 16 | "laravel/framework": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 17 | "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 18 | "illuminate/routing": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 19 | "litesaml/lightsaml": "^4.0", 20 | "ext-zlib": "*" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "CodeGreenCreative\\SamlIdp\\": "src/" 25 | } 26 | }, 27 | "extra": { 28 | "laravel": { 29 | "providers": [ 30 | "CodeGreenCreative\\SamlIdp\\LaravelSamlIdpServiceProvider" 31 | ] 32 | } 33 | }, 34 | "require-dev": { 35 | "laravel/pint": "^1.17" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /config/samlidp.php: -------------------------------------------------------------------------------- 1 | false, 16 | // Define the email address field name in the users table 17 | 'email_field' => 'email', 18 | // Define the Name ID for the email field. 19 | 'email_name_id'=> SamlConstants::NAME_ID_FORMAT_EMAIL, 20 | // Define the name field in the users table 21 | 'name_field' => 'name', 22 | // The URI to your login page 23 | 'login_uri' => 'login', 24 | // Log out of the IdP after SLO 25 | 'logout_after_slo' => env('LOGOUT_AFTER_SLO', false), 26 | // The URI to the saml metadata file, this describes your idP 27 | 'issuer_uri' => 'saml/metadata', 28 | // The certificate 29 | 'cert' => env('SAMLIDP_CERT'), 30 | // Name of the certificate PEM file, ignored if cert is used 31 | 'certname' => 'cert.pem', 32 | // The certificate key 33 | 'key' => env('SAMLIDP_KEY'), 34 | // Name of the certificate key PEM file, ignored if key is used 35 | 'keyname' => 'key.pem', 36 | // Encrypt requests and responses 37 | 'encrypt_assertion' => true, 38 | // Make sure messages are signed 39 | 'messages_signed' => true, 40 | // Defind what digital algorithm you want to use 41 | 'digest_algorithm' => \RobRichards\XMLSecLibs\XMLSecurityDSig::SHA1, 42 | // list of all service providers 43 | 'sp' => [ 44 | // Base64 encoded ACS URL 45 | // 'aHR0cHM6Ly9teWZhY2Vib29rd29ya3BsYWNlLmZhY2Vib29rLmNvbS93b3JrL3NhbWwucGhw' => [ 46 | // // Your destination is the ACS URL of the Service Provider 47 | // 'destination' => 'https://myfacebookworkplace.facebook.com/work/saml.php', 48 | // 'logout' => 'https://myfacebookworkplace.facebook.com/work/sls.php', 49 | // // SP certificate 50 | // 'certificate' => '', 51 | // // Turn off auto appending of the idp query param 52 | // 'query_params' => false, 53 | // // Turn off the encryption of the assertion per SP 54 | // 'encrypt_assertion' => false 55 | // ] 56 | ], 57 | 58 | // If you need to redirect after SLO depending on SLO initiator 59 | // key is beginning of HTTP_REFERER value from SERVER, value is redirect path 60 | 'sp_slo_redirects' => [ 61 | // 'https://example.com' => 'https://example.com', 62 | ], 63 | 64 | // All of the Laravel SAML IdP event / listener mappings. 65 | 'events' => [ 66 | 'CodeGreenCreative\SamlIdp\Events\Assertion' => [], 67 | 'Illuminate\Auth\Events\Logout' => ['CodeGreenCreative\SamlIdp\Listeners\SamlLogout'], 68 | 'Illuminate\Auth\Events\Authenticated' => ['CodeGreenCreative\SamlIdp\Listeners\SamlAuthenticated'], 69 | 'Illuminate\Auth\Events\Login' => ['CodeGreenCreative\SamlIdp\Listeners\SamlLogin'], 70 | ], 71 | 72 | // List of guards saml idp will catch Authenticated, Login and Logout events 73 | 'guards' => ['web'], 74 | ]; 75 | -------------------------------------------------------------------------------- /database/migrations/2025_01_29_095333_create_saml_service_providers_table.php: -------------------------------------------------------------------------------- 1 | string('id')->unique()->comment('acs_url base64 encoded'); 16 | $table->text('acs_url'); 17 | $table->text('destination'); 18 | $table->text('logout'); 19 | $table->longText('certificate'); 20 | $table->boolean('query_params'); 21 | $table->boolean('encrypt_assertion'); 22 | $table->timestamps(); 23 | 24 | $table->primary('id'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('laravel_samlidp_service_providers'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-samlidp", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "@prettier/plugin-php": "^0.18.9", 9 | "prettier": "^2.7.1" 10 | } 11 | }, 12 | "node_modules/@prettier/plugin-php": { 13 | "version": "0.18.9", 14 | "resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.18.9.tgz", 15 | "integrity": "sha512-d1uE9v3JsQ9uBLWlssZhO02RLI8u8jRBFPCO5ud4VbveCpSZbDRnGpSuK3IqSWHdM/OnuySz0yWr5M9/9mINvw==", 16 | "dev": true, 17 | "dependencies": { 18 | "linguist-languages": "^7.21.0", 19 | "mem": "^8.0.0", 20 | "php-parser": "3.1.0-beta.11" 21 | }, 22 | "peerDependencies": { 23 | "prettier": "^1.15.0 || ^2.0.0" 24 | } 25 | }, 26 | "node_modules/linguist-languages": { 27 | "version": "7.21.0", 28 | "resolved": "https://registry.npmjs.org/linguist-languages/-/linguist-languages-7.21.0.tgz", 29 | "integrity": "sha512-KrWJJbFOvlDhjlt5OhUipVlXg+plUfRurICAyij1ZVxQcqPt/zeReb9KiUVdGUwwhS/2KS9h3TbyfYLA5MDlxQ==", 30 | "dev": true 31 | }, 32 | "node_modules/map-age-cleaner": { 33 | "version": "0.1.3", 34 | "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", 35 | "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", 36 | "dev": true, 37 | "dependencies": { 38 | "p-defer": "^1.0.0" 39 | }, 40 | "engines": { 41 | "node": ">=6" 42 | } 43 | }, 44 | "node_modules/mem": { 45 | "version": "8.1.1", 46 | "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", 47 | "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", 48 | "dev": true, 49 | "dependencies": { 50 | "map-age-cleaner": "^0.1.3", 51 | "mimic-fn": "^3.1.0" 52 | }, 53 | "engines": { 54 | "node": ">=10" 55 | }, 56 | "funding": { 57 | "url": "https://github.com/sindresorhus/mem?sponsor=1" 58 | } 59 | }, 60 | "node_modules/mimic-fn": { 61 | "version": "3.1.0", 62 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", 63 | "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", 64 | "dev": true, 65 | "engines": { 66 | "node": ">=8" 67 | } 68 | }, 69 | "node_modules/p-defer": { 70 | "version": "1.0.0", 71 | "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", 72 | "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", 73 | "dev": true, 74 | "engines": { 75 | "node": ">=4" 76 | } 77 | }, 78 | "node_modules/php-parser": { 79 | "version": "3.1.0-beta.11", 80 | "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.1.0-beta.11.tgz", 81 | "integrity": "sha512-aKhWHXun6FKa0MX+GcJtEoLPSWuGQTiEkNgckVjT95OAnKG33c+zsDQEpXx4R74PQ030YZLNq9XV7odKapbOsg==", 82 | "dev": true 83 | }, 84 | "node_modules/prettier": { 85 | "version": "2.7.1", 86 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", 87 | "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", 88 | "dev": true, 89 | "bin": { 90 | "prettier": "bin-prettier.js" 91 | }, 92 | "engines": { 93 | "node": ">=10.13.0" 94 | }, 95 | "funding": { 96 | "url": "https://github.com/prettier/prettier?sponsor=1" 97 | } 98 | } 99 | }, 100 | "dependencies": { 101 | "@prettier/plugin-php": { 102 | "version": "0.18.9", 103 | "resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.18.9.tgz", 104 | "integrity": "sha512-d1uE9v3JsQ9uBLWlssZhO02RLI8u8jRBFPCO5ud4VbveCpSZbDRnGpSuK3IqSWHdM/OnuySz0yWr5M9/9mINvw==", 105 | "dev": true, 106 | "requires": { 107 | "linguist-languages": "^7.21.0", 108 | "mem": "^8.0.0", 109 | "php-parser": "3.1.0-beta.11" 110 | } 111 | }, 112 | "linguist-languages": { 113 | "version": "7.21.0", 114 | "resolved": "https://registry.npmjs.org/linguist-languages/-/linguist-languages-7.21.0.tgz", 115 | "integrity": "sha512-KrWJJbFOvlDhjlt5OhUipVlXg+plUfRurICAyij1ZVxQcqPt/zeReb9KiUVdGUwwhS/2KS9h3TbyfYLA5MDlxQ==", 116 | "dev": true 117 | }, 118 | "map-age-cleaner": { 119 | "version": "0.1.3", 120 | "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", 121 | "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", 122 | "dev": true, 123 | "requires": { 124 | "p-defer": "^1.0.0" 125 | } 126 | }, 127 | "mem": { 128 | "version": "8.1.1", 129 | "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", 130 | "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", 131 | "dev": true, 132 | "requires": { 133 | "map-age-cleaner": "^0.1.3", 134 | "mimic-fn": "^3.1.0" 135 | } 136 | }, 137 | "mimic-fn": { 138 | "version": "3.1.0", 139 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", 140 | "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", 141 | "dev": true 142 | }, 143 | "p-defer": { 144 | "version": "1.0.0", 145 | "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", 146 | "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", 147 | "dev": true 148 | }, 149 | "php-parser": { 150 | "version": "3.1.0-beta.11", 151 | "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.1.0-beta.11.tgz", 152 | "integrity": "sha512-aKhWHXun6FKa0MX+GcJtEoLPSWuGQTiEkNgckVjT95OAnKG33c+zsDQEpXx4R74PQ030YZLNq9XV7odKapbOsg==", 153 | "dev": true 154 | }, 155 | "prettier": { 156 | "version": "2.7.1", 157 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", 158 | "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", 159 | "dev": true 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@prettier/plugin-php": "^0.18.9", 5 | "prettier": "^2.7.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "ordered_imports": { 5 | "sort_algorithm": "length" 6 | }, 7 | "function_declaration": { 8 | "closure_fn_spacing": "none" 9 | }, 10 | "concat_space": { 11 | "spacing": "one" 12 | }, 13 | "not_operator_with_successor_space": false, 14 | "no_spaces_after_function_name": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /resources/views/components/input.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/metadata.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $cert }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ $cert }} 15 | 16 | 17 | 18 | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress 19 | 20 | 21 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | only('index'); 6 | Route::resource('logout', 'LogoutController')->only('index'); 7 | -------------------------------------------------------------------------------- /src/Console/CreateCertificate.php: -------------------------------------------------------------------------------- 1 | option('days'); 45 | $keyname = $this->option('keyname'); 46 | $certname = $this->option('certname'); 47 | 48 | // Create storage/samlidp directory 49 | if (!file_exists($storagePath)) { 50 | mkdir($storagePath, 0755, true); 51 | } 52 | 53 | $key = sprintf('%s/%s', $storagePath, $keyname); 54 | $cert = sprintf('%s/%s', $storagePath, $certname); 55 | $question = 'The name chosen for the PEM files already exist. Would you like to overwrite existing PEM files?'; 56 | if ((!file_exists($key) && !file_exists($cert)) || $this->confirm($question)) { 57 | $command = 'openssl req -x509 -sha256 -nodes -days %s -newkey rsa:2048 -keyout %s -out %s'; 58 | exec(sprintf($command, $days, $key, $cert)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Console/CreateServiceProvider.php: -------------------------------------------------------------------------------- 1 | ask('What is the service provider ACS URL?'); 41 | $logoutUrl = $this->ask('What is the service provider logout URL?'); 42 | 43 | $encodedAcsUrl = base64_encode($acsUrl); 44 | 45 | $this->line('SamlIdp config:'); 46 | $this->line(''); 47 | $this->line("'{$encodedAcsUrl}' => ["); 48 | $this->line(" 'destination' => '{$acsUrl}',"); 49 | $this->line(" 'logout' => '{$logoutUrl}',"); 50 | $this->line("]"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Contracts/SamlContract.php: -------------------------------------------------------------------------------- 1 | attribute_statement = &$attribute_statement; 31 | $this->attribute_statement 32 | ->addAttribute(new Attribute(ClaimTypes::EMAIL_ADDRESS, auth($guard)->user()->__get(config('samlidp.email_field', 'email')))) 33 | ->addAttribute(new Attribute(ClaimTypes::COMMON_NAME, auth($guard)->user()->__get(config('samlidp.name_field', 'name')))); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exceptions/DestinationMissingException.php: -------------------------------------------------------------------------------- 1 | session()->get('saml.slo_redirect'); 22 | if (!$slo_redirect) { 23 | $this->setSloRedirect($request); 24 | $slo_redirect = $request->session()->get('saml.slo_redirect'); 25 | } 26 | 27 | if (null === $request->session()->get('saml.slo')) { 28 | $request->session()->put('saml.slo', []); 29 | } 30 | 31 | // Need to broadcast to our other SAML apps to log out! 32 | // Loop through our service providers and "touch" the logout URL's 33 | foreach ($this->getAllServiceProviders() as $key => $sp) { 34 | // Check if the service provider supports SLO 35 | if (!empty($sp['logout']) && !in_array($key, $request->session()->get('saml.slo', []))) { 36 | // Push this SP onto the saml slo array 37 | $request->session()->push('saml.slo', $key); 38 | return redirect(SamlSlo::dispatchSync($sp)); 39 | } 40 | } 41 | 42 | if (config('samlidp.logout_after_slo')) { 43 | auth()->logout(); 44 | $request->session()->invalidate(); 45 | } 46 | 47 | $request->session()->forget('saml.slo'); 48 | $request->session()->forget('saml.slo_redirect'); 49 | 50 | return redirect($slo_redirect); 51 | } 52 | 53 | private function setSloRedirect(Request $request) 54 | { 55 | // Look for return_to query in case of not relying on HTTP_REFERER 56 | $http_referer = $request->has('return_to') ? $request->get('return_to') : $request->server('HTTP_REFERER'); 57 | $redirects = config('samlidp.sp_slo_redirects', []); 58 | $slo_redirect = config('samlidp.login_uri'); 59 | foreach ($redirects as $referer => $redirectPath) { 60 | if (Str::startsWith($http_referer, $referer)) { 61 | $slo_redirect = $redirectPath; 62 | break; 63 | } 64 | } 65 | 66 | $request->session()->put('saml.slo_redirect', $slo_redirect); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Http/Controllers/MetadataController.php: -------------------------------------------------------------------------------- 1 | get(config('samlidp.certname', 'cert.pem')); 23 | 24 | if (strpos($cert, 'file://') === 0) { 25 | if (!is_file($cert)) { 26 | throw new \InvalidArgumentException(sprintf("File not found '%s'", $cert)); 27 | } 28 | $cert = file_get_contents($cert); 29 | } 30 | 31 | $cert = preg_replace('/^\W+\w+\s+\w+\W+\s(.*)\s+\W+.*$/s', '$1', trim($cert)); 32 | $cert = str_replace(PHP_EOL, "", $cert); 33 | 34 | return response(view('samlidp::metadata', compact('cert')), 200, [ 35 | 'Content-Type' => 'application/xml', 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Jobs/SamlSlo.php: -------------------------------------------------------------------------------- 1 | sp = $sp; 39 | $this->init(); 40 | } 41 | 42 | /** 43 | * [handle description] 44 | * 45 | * @param [type] $sp [description] 46 | * @return [type] [description] 47 | */ 48 | public function handle() 49 | { 50 | $this->setDestination(); 51 | // We are receiving a Logout Request 52 | if (request()->filled('SAMLRequest')) { 53 | $xml = gzinflate(base64_decode(request('SAMLRequest'))); 54 | $deserializationContext = new DeserializationContext; 55 | $deserializationContext->getDocument()->loadXML($xml); 56 | // Get the final destination 57 | session()->put('RelayState', request('RelayState')); 58 | } elseif (request()->filled('SAMLResponse')) { 59 | $xml = gzinflate(base64_decode(request('SAMLResponse'))); 60 | $deserializationContext = new DeserializationContext; 61 | $deserializationContext->getDocument()->loadXML($xml); 62 | } 63 | 64 | // Send the request to log out 65 | return $this->request(); 66 | } 67 | 68 | /** 69 | * [response description] 70 | * 71 | * @return [type] [description] 72 | */ 73 | public function response() 74 | { 75 | $this->response = (new LogoutResponse) 76 | ->setIssuer(new Issuer($this->issuer)) 77 | ->setID(Helper::generateID()) 78 | ->setIssueInstant(new \DateTime) 79 | ->setDestination($this->destination) 80 | ->setInResponseTo($this->logout_request->getId()) 81 | ->setStatus(new Status(new StatusCode('urn:oasis:names:tc:SAML:2.0:status:Success'))); 82 | 83 | if (config('samlidp.messages_signed')) { 84 | $this->response->setSignature( 85 | new SignatureWriter($this->certificate, $this->private_key, $this->digest_algorithm) 86 | ); 87 | } 88 | 89 | return $this->send(SamlConstants::BINDING_SAML2_HTTP_REDIRECT); 90 | } 91 | 92 | /** 93 | * [request description] 94 | * 95 | * @return [type] [description] 96 | */ 97 | public function request() 98 | { 99 | $this->response = (new LogoutRequest) 100 | ->setIssuer(new Issuer($this->issuer)) 101 | ->setNameID(new NameID(Helper::generateID(), SamlConstants::NAME_ID_FORMAT_TRANSIENT)) 102 | ->setID(Helper::generateID()) 103 | ->setIssueInstant(new \DateTime) 104 | ->setDestination($this->destination); 105 | 106 | if (config('samlidp.messages_signed')) { 107 | $this->response->setSignature( 108 | new SignatureWriter($this->certificate, $this->private_key, $this->digest_algorithm) 109 | ); 110 | } 111 | 112 | return $this->send(SamlConstants::BINDING_SAML2_HTTP_REDIRECT); 113 | } 114 | 115 | private function setDestination() 116 | { 117 | $destination = $this->sp['logout']; 118 | $queryParams = $this->getQueryParams(); 119 | if (!empty($queryParams)) { 120 | if (!parse_url($destination, PHP_URL_QUERY)) { 121 | $destination = Str::finish(url($destination), '?') . Arr::query($queryParams); 122 | } else { 123 | $destination .= '&' . Arr::query($queryParams); 124 | } 125 | } 126 | 127 | $this->destination = $destination; 128 | } 129 | 130 | private function getQueryParams() 131 | { 132 | $queryParams = isset($this->sp['query_params']) ? $this->sp['query_params'] : null; 133 | 134 | if (is_null($queryParams)) { 135 | $queryParams = [ 136 | 'idp' => config('app.url'), 137 | ]; 138 | } 139 | 140 | return $queryParams; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Jobs/SamlSso.php: -------------------------------------------------------------------------------- 1 | guard = $guard; 56 | $this->init(); 57 | } 58 | 59 | /** 60 | * Execute the job. 61 | * 62 | * @return void 63 | */ 64 | public function handle() 65 | { 66 | $deserializationContext = new DeserializationContext; 67 | $deserializationContext->getDocument()->loadXML(gzinflate(base64_decode(request('SAMLRequest')))); 68 | 69 | $this->authn_request = new AuthnRequest; 70 | $this->authn_request->deserialize($deserializationContext->getDocument()->firstChild, $deserializationContext); 71 | 72 | $this->setDestination(); 73 | 74 | return $this->response(); 75 | } 76 | 77 | public function response() 78 | { 79 | $this->response = (new Response) 80 | ->setIssuer(new Issuer($this->issuer)) 81 | ->setStatus(new Status(new StatusCode('urn:oasis:names:tc:SAML:2.0:status:Success'))) 82 | ->setID(Helper::generateID()) 83 | ->setIssueInstant(new \DateTime) 84 | ->setDestination($this->destination) 85 | ->setInResponseTo($this->authn_request->getId()); 86 | 87 | $assertion = new Assertion; 88 | $assertion 89 | ->setId(Helper::generateID()) 90 | ->setIssueInstant(new \DateTime) 91 | ->setIssuer(new Issuer($this->issuer)) 92 | ->setSignature(new SignatureWriter($this->certificate, $this->private_key, $this->digest_algorithm)) 93 | ->setSubject( 94 | (new Subject) 95 | ->setNameID( 96 | new NameID( 97 | auth($this->guard) 98 | ->user() 99 | ->__get(config('samlidp.email_field', 'email')), 100 | config('samlidp.email_name_id', SamlConstants::NAME_ID_FORMAT_EMAIL) 101 | ) 102 | ) 103 | ->addSubjectConfirmation( 104 | (new SubjectConfirmation) 105 | ->setMethod(SamlConstants::CONFIRMATION_METHOD_BEARER) 106 | ->setSubjectConfirmationData( 107 | (new SubjectConfirmationData) 108 | ->setInResponseTo($this->authn_request->getId()) 109 | ->setNotOnOrAfter(new \DateTime('+1 MINUTE')) 110 | ->setRecipient($this->authn_request->getAssertionConsumerServiceURL()) 111 | ) 112 | ) 113 | ) 114 | ->setConditions( 115 | (new Conditions) 116 | ->setNotBefore(new \DateTime) 117 | ->setNotOnOrAfter(new \DateTime('+1 MINUTE')) 118 | ->addItem(new AudienceRestriction([$this->authn_request->getIssuer()->getValue()])) 119 | ) 120 | ->addItem( 121 | (new AuthnStatement) 122 | ->setAuthnInstant(new \DateTime('-10 MINUTE')) 123 | ->setSessionIndex(Helper::generateID()) 124 | ->setAuthnContext( 125 | (new AuthnContext)->setAuthnContextClassRef(SamlConstants::NAME_ID_FORMAT_UNSPECIFIED) 126 | ) 127 | ); 128 | 129 | $attribute_statement = new AttributeStatement; 130 | event(new AssertionEvent($attribute_statement, $this->guard)); 131 | // Add the attributes to the assertion 132 | $assertion->addItem($attribute_statement); 133 | 134 | // Encrypt the assertion 135 | 136 | if ($this->encryptAssertion()) { 137 | $encryptedAssertion = new EncryptedAssertionWriter; 138 | $encryptedAssertion->encrypt($assertion, KeyHelper::createPublicKey($this->getSpCertificate())); 139 | $this->response->addEncryptedAssertion($encryptedAssertion); 140 | } else { 141 | $this->response->addAssertion($assertion); 142 | } 143 | 144 | if (config('samlidp.messages_signed')) { 145 | $this->response->setSignature( 146 | new SignatureWriter($this->certificate, $this->private_key, $this->digest_algorithm) 147 | ); 148 | } 149 | 150 | return $this->send(SamlConstants::BINDING_SAML2_HTTP_POST); 151 | } 152 | 153 | /** 154 | * [sendSamlRequest description] 155 | * 156 | * @param Request $request [description] 157 | * @param User $user [description] 158 | * @return [type] [description] 159 | */ 160 | public function send($binding_type) 161 | { 162 | $bindingFactory = new BindingFactory; 163 | $postBinding = $bindingFactory->create($binding_type); 164 | $messageContext = new MessageContext; 165 | $messageContext->setMessage($this->response)->asResponse(); 166 | $message = $messageContext->getMessage(); 167 | $message->setRelayState(request('RelayState')); 168 | $httpResponse = $postBinding->send($messageContext); 169 | 170 | return $httpResponse->getContent(); 171 | } 172 | 173 | private function setDestination() 174 | { 175 | $destination = $this->getServiceProviderConfigValue($this->authn_request, 'destination'); 176 | 177 | if (empty($destination)) { 178 | throw new DestinationMissingException( 179 | sprintf( 180 | '%s does not have a destination set in config file.', 181 | $this->getServiceProvider($this->authn_request) 182 | ) 183 | ); 184 | } 185 | 186 | $queryParams = $this->getQueryParams(); 187 | if (is_array($queryParams) && !empty($queryParams)) { 188 | if (!parse_url($destination, PHP_URL_QUERY)) { 189 | $destination = Str::finish(url($destination), '?') . Arr::query($queryParams); 190 | } else { 191 | $destination .= '&' . Arr::query($queryParams); 192 | } 193 | } 194 | 195 | $this->destination = $destination; 196 | } 197 | 198 | private function getQueryParams() 199 | { 200 | $queryParams = $this->getServiceProviderConfigValue($this->authn_request, 'query_params'); 201 | 202 | if (is_null($queryParams)) { 203 | $queryParams = [ 204 | 'idp' => config('app.url'), 205 | ]; 206 | } 207 | 208 | return $queryParams; 209 | } 210 | 211 | private function getSpCertificate() 212 | { 213 | $spCertificate = $this->getServiceProviderConfigValue($this->authn_request, 'certificate'); 214 | 215 | return strpos($spCertificate, 'file://') === 0 216 | ? X509Certificate::fromFile($spCertificate) 217 | : (new X509Certificate)->loadPem($spCertificate); 218 | } 219 | 220 | /** 221 | * Check to see if the SP wants to encrypt assertions first 222 | * If its not set, default to base encryption assertion config 223 | * Otherwise return true 224 | */ 225 | private function encryptAssertion(): bool 226 | { 227 | return $this->getServiceProviderConfigValue($this->authn_request, 'encrypt_assertion') 228 | ?? config('samlidp.encrypt_assertion', true); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/LaravelSamlIdpServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerEvents(); 32 | $this->registerRoutes(); 33 | $this->registerMigrations(); 34 | $this->registerResources(); 35 | $this->registerBladeComponents(); 36 | } 37 | 38 | /** 39 | * Register the service provider. 40 | * 41 | * @return void 42 | */ 43 | public function register() 44 | { 45 | $this->configure(); 46 | $this->offerPublishing(); 47 | $this->registerServices(); 48 | $this->registerCommands(); 49 | } 50 | 51 | /** 52 | * Configure the service provider 53 | * 54 | * @return void 55 | */ 56 | private function configure() 57 | { 58 | $this->mergeConfigFrom(__DIR__ . '/../config/samlidp.php', 'samlidp'); 59 | } 60 | 61 | /** 62 | * Offer publishing for the service provider 63 | * 64 | * @return void 65 | */ 66 | public function offerPublishing() 67 | { 68 | if ($this->app->runningInConsole()) { 69 | $this->publishes([ 70 | __DIR__ . '/../resources/views' => resource_path('views/vendor/samlidp'), 71 | ], 'samlidp_views'); 72 | 73 | $this->publishes([ 74 | __DIR__ . '/../config/samlidp.php' => config_path('samlidp.php'), 75 | ], 'samlidp_config'); 76 | } 77 | } 78 | 79 | /** 80 | * Register blade components for service provider 81 | * 82 | * @return void 83 | */ 84 | public function registerBladeComponents() 85 | { 86 | Blade::directive('samlidp', function ($expression) { 87 | return "filled('SAMLRequest') ? view('samlidp::components.input') : ''; ?>"; 88 | }); 89 | } 90 | 91 | /** 92 | * Register the application bindings. 93 | * 94 | * @return void 95 | */ 96 | private function registerServices() 97 | { 98 | } 99 | 100 | /** 101 | * Loop through events and listeners provided by EventMap trait 102 | * 103 | * @return void 104 | */ 105 | private function registerEvents() 106 | { 107 | $events = $this->app->make(Dispatcher::class); 108 | foreach (config('samlidp.events', $this->default_events) as $event => $listeners) { 109 | foreach ($listeners as $listener) { 110 | $events->listen($event, $listener); 111 | } 112 | } 113 | } 114 | 115 | /** 116 | * Register routes for the service provider 117 | * 118 | * @return void 119 | */ 120 | private function registerRoutes() 121 | { 122 | Route::name('saml.') 123 | ->prefix('saml') 124 | ->namespace('CodeGreenCreative\SamlIdp\Http\Controllers') 125 | ->middleware('web')->group(function () { 126 | $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); 127 | }); 128 | } 129 | 130 | /** 131 | * Register resources for the service provider 132 | * 133 | * @return void 134 | */ 135 | private function registerResources() 136 | { 137 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'samlidp'); 138 | } 139 | 140 | /** 141 | * Register the artisan commands. 142 | * 143 | * @return void 144 | */ 145 | private function registerCommands() 146 | { 147 | if ($this->app->runningInConsole()) { 148 | $this->commands([ 149 | CreateCertificate::class, 150 | CreateServiceProvider::class, 151 | ]); 152 | } 153 | } 154 | 155 | private function registerMigrations() 156 | { 157 | $this->publishesMigrations([ 158 | __DIR__.'/../database/migrations' => database_path('migrations'), 159 | ]); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Listeners/SamlAuthenticated.php: -------------------------------------------------------------------------------- 1 | guard, config('samlidp.guards')) && 20 | request()->filled('SAMLRequest') && 21 | !request()->is('saml/logout') && 22 | request()->isMethod('get') 23 | ) { 24 | abort(response(SamlSso::dispatchSync($event->guard), 302)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Listeners/SamlLogin.php: -------------------------------------------------------------------------------- 1 | guard, config('samlidp.guards')) && 20 | request()->filled('SAMLRequest') && 21 | !request()->is('saml/logout') 22 | ) { 23 | abort(response(SamlSso::dispatchSync($event->guard), 302)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listeners/SamlLogout.php: -------------------------------------------------------------------------------- 1 | guard, config('samlidp.guards')) && null === session('saml.slo')) { 20 | abort(redirect('saml/logout'), 200); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Models/SamlServiceProvider.php: -------------------------------------------------------------------------------- 1 | [], 14 | 'Illuminate\Auth\Events\Logout' => [ 15 | 'CodeGreenCreative\SamlIdp\Listeners\SamlLogout', 16 | ], 17 | 'Illuminate\Auth\Events\Authenticated' => [ 18 | 'CodeGreenCreative\SamlIdp\Listeners\SamlAuthenticated', 19 | ], 20 | 'Illuminate\Auth\Events\Login' => [ 21 | 'CodeGreenCreative\SamlIdp\Listeners\SamlLogin', 22 | ], 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /src/Traits/PerformsSingleSignOn.php: -------------------------------------------------------------------------------- 1 | issuer = url(config('samlidp.issuer_uri')); 29 | $this->certificate = $this->getCertificate(); 30 | $this->private_key = $this->getKey(); 31 | $this->digest_algorithm = config('samlidp.digest_algorithm', XMLSecurityDSig::SHA1); 32 | } 33 | 34 | /** 35 | * Send a SAML response/request 36 | * 37 | * @param string $binding_type 38 | * @param string $as 39 | * @return string Target URL 40 | */ 41 | protected function send($binding_type, $as = 'asResponse') 42 | { 43 | // The response will be to the sls URL of the SP 44 | $bindingFactory = new BindingFactory; 45 | $binding = $bindingFactory->create($binding_type); 46 | $messageContext = new MessageContext(); 47 | $messageContext->setMessage($this->response)->$as(); 48 | $message = $messageContext->getMessage(); 49 | if (! empty(request()->filled('RelayState'))) { 50 | $message->setRelayState(request('RelayState')); 51 | } 52 | $httpResponse = $binding->send($messageContext); 53 | // Just return the target URL for proper redirection 54 | return $httpResponse->getTargetUrl(); 55 | } 56 | 57 | /** 58 | * Get service provider from AuthNRequest 59 | * 60 | * @return string 61 | */ 62 | public function getServiceProvider($request) 63 | { 64 | return base64_encode($request->getAssertionConsumerServiceURL()); 65 | } 66 | 67 | /** 68 | * @return \LightSaml\Credential\X509Certificate 69 | */ 70 | protected function getCertificate(): X509Certificate 71 | { 72 | $certificate = config('samlidp.cert') ?: Storage::disk('samlidp')->get(config('samlidp.certname', 'cert.pem')); 73 | 74 | return (strpos($certificate, 'file://') === 0) 75 | ? X509Certificate::fromFile($certificate) 76 | : (new X509Certificate)->loadPem($certificate); 77 | } 78 | 79 | /** 80 | * @return \RobRichards\XMLSecLibs\XMLSecurityKey 81 | */ 82 | protected function getKey(): XMLSecurityKey 83 | { 84 | $key = config('samlidp.key') ?: Storage::disk('samlidp')->get(config('samlidp.keyname', 'key.pem')); 85 | 86 | return KeyHelper::createPrivateKey($key, '', strpos($key, 'file://') === 0, XMLSecurityKey::RSA_SHA256); 87 | } 88 | 89 | protected function getServiceProviderConfigValue($request, string $configKey): mixed 90 | { 91 | if (config('samlidp.sp') === SamlServiceProvider::class) { 92 | $serviceProvider = SamlServiceProvider::findOrFail($this->getServiceProvider($request)); 93 | 94 | return $serviceProvider->$configKey; 95 | } 96 | 97 | return config(sprintf('samlidp.sp.%s.%s', $this->getServiceProvider($request), $configKey)); 98 | } 99 | 100 | public function getAllServiceProviders(): array 101 | { 102 | if (config('samlidp.sp') === SamlServiceProvider::class) { 103 | return SamlServiceProvider::all()->toArray(); 104 | } 105 | 106 | return config('samlidp.sp'); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Traits/SamlidpLog.php: -------------------------------------------------------------------------------- 1 |