├── LICENSE ├── README.md ├── composer.json ├── config └── saml2.php ├── database └── migrations │ ├── 2019_06_24_140207_create_saml2_tenants_table.php │ ├── 2020_10_22_140856_add_relay_state_url_column_to_saml2_tenants_table.php │ └── 2020_10_23_072902_add_name_id_format_column_to_saml2_tenants_table.php ├── public └── .gitkeep └── src ├── Auth.php ├── Commands ├── CreateTenant.php ├── DeleteTenant.php ├── ListTenants.php ├── RendersTenants.php ├── RestoreTenant.php ├── TenantCredentials.php ├── UpdateTenant.php └── ValidatesInput.php ├── Events ├── SignedIn.php └── SignedOut.php ├── Facades └── Auth.php ├── Helpers └── ConsoleHelper.php ├── Http ├── Controllers │ └── Saml2Controller.php ├── Middleware │ └── ResolveTenant.php └── routes.php ├── Models └── Tenant.php ├── OneLoginBuilder.php ├── Repositories └── TenantRepository.php ├── Saml2User.php ├── ServiceProvider.php └── helpers.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 24Slides 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [Laravel 5.4+] SAML Service Provider 2 | 3 | [![Latest Stable Version][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Build Status][ico-travis]][link-travis] 6 | [![Quality Score][ico-code-quality]][link-code-quality] 7 | [![Code Coverage][ico-code-coverage]][link-code-coverage] 8 | [![Total Downloads][ico-downloads]][link-downloads] 9 | 10 | An integration to add SSO to your service via SAML2 protocol based on [OneLogin](https://github.com/onelogin/php-saml) toolkit. 11 | 12 | This package turns your application into Service Provider with the support of multiple Identity Providers. 13 | 14 | ## Requirements 15 | 16 | - Laravel 5.4+ 17 | - PHP 7.0+ 18 | 19 | ## Getting Started 20 | 21 | ### Installing 22 | 23 | ##### Step 1. Install dependency 24 | 25 | ``` 26 | composer require 24slides/laravel-saml2 27 | ``` 28 | 29 | If you are using Laravel 5.5 and higher, the service provider will be automatically registered. 30 | 31 | For older versions, you have to add the service provider and alias to your `config/app.php`: 32 | 33 | ```php 34 | 'providers' => [ 35 | ... 36 | Slides\Saml2\ServiceProvider::class, 37 | ] 38 | 39 | 'alias' => [ 40 | ... 41 | 'Saml2' => Slides\Saml2\Facades\Auth::class, 42 | ] 43 | ``` 44 | 45 | ##### Step 2. Publish the configuration file. 46 | 47 | ``` 48 | php artisan vendor:publish --provider="Slides\Saml2\ServiceProvider" 49 | ``` 50 | 51 | ##### Step 3. Run migrations 52 | 53 | ``` 54 | php artisan migrate 55 | ``` 56 | 57 | ### Configuring 58 | 59 | Once you publish `saml2.php` to `app/config`, you need to configure your SP. Most of options are inherited from [OneLogin Toolkit](https://github.com/onelogin/php-saml), so you can check documentation there. 60 | 61 | #### Identity Providers (IdPs) 62 | 63 | To distinguish between identity providers there is an entity called Tenant that represent each IdP. 64 | 65 | When request comes to an application, the middleware parses UUID and resolves the Tenant. 66 | 67 | You can easily manage tenants using the following console commands: 68 | 69 | - `artisan saml2:create-tenant` 70 | - `artisan saml2:update-tenant` 71 | - `artisan saml2:delete-tenant` 72 | - `artisan saml2:restore-tenant` 73 | - `artisan saml2:list-tenants` 74 | - `artisan saml2:tenant-credentials` 75 | 76 | > To learn their options, run a command with `-h` parameter. 77 | 78 | Each Tenant has the following attributes: 79 | 80 | - **UUID** — a unique identifier that allows to resolve a tenannt and configure SP correspondingly 81 | - **Key** — a custom key to use for application needs 82 | - **Entity ID** — [Identity Provider Entity ID](https://spaces.at.internet2.edu/display/InCFederation/Entity+IDs) 83 | - **Login URL** — Identity Provider Single Sign On URL 84 | - **Logout URL** — Identity Provider Logout URL 85 | - **x509 certificate** — The certificate provided by Identity Provider in **base64** format 86 | - **Metadata** — Custom parameters for your application needs 87 | 88 | #### Default routes 89 | 90 | The following routes are registered by default: 91 | 92 | - `GET saml2/{uuid}/login` 93 | - `GET saml2/{uuid}/logout` 94 | - `GET saml2/{uuid}/metadata` 95 | - `POST saml2/{uuid}/acs` 96 | - `POST saml2/{uuid}/sls` 97 | 98 | You may disable them by setting `saml2.useRoutes` to `false`. 99 | 100 | > `/saml2` prefix can be changed via `saml2.routesPrefix` config parameter. 101 | 102 | ## Usage 103 | 104 | ### Authentication events 105 | 106 | The simplest way to handle SAML authentication is to add listeners on `Slides\Saml2\SignedIn` and `Slides\Saml2\SignedOut` events. 107 | 108 | ```php 109 | Event::listen(\Slides\Saml2\Events\SignedIn::class, function (\Slides\Saml2\Events\SignedIn $event) { 110 | $messageId = $event->getAuth()->getLastMessageId(); 111 | 112 | // your own code preventing reuse of a $messageId to stop replay attacks 113 | $samlUser = $event->getSaml2User(); 114 | 115 | $userData = [ 116 | 'id' => $samlUser->getUserId(), 117 | 'attributes' => $samlUser->getAttributes(), 118 | 'assertion' => $samlUser->getRawSamlAssertion() 119 | ]; 120 | 121 | $user = // find user by ID or attribute 122 | 123 | // Login a user. 124 | Auth::login($user); 125 | }); 126 | ``` 127 | 128 | ### Middleware 129 | 130 | To define a middleware for default routes, add its name to `config/saml2.php`: 131 | 132 | ```php 133 | /* 134 | |-------------------------------------------------------------------------- 135 | | Built-in routes prefix 136 | |-------------------------------------------------------------------------- 137 | | 138 | | Here you may define the prefix for built-in routes. 139 | | 140 | */ 141 | 142 | 'routesMiddleware' => ['saml'], 143 | ``` 144 | 145 | Then you need to define necessary middlewares for your group in `app/Http/Kernel.php`: 146 | 147 | ```php 148 | protected $middlewareGroups = [ 149 | 'web' => [ 150 | ... 151 | ], 152 | 'api' => [ 153 | ... 154 | ], 155 | 'saml' => [ 156 | \App\Http\Middleware\EncryptCookies::class, 157 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 158 | \Illuminate\Session\Middleware\StartSession::class, 159 | ], 160 | ``` 161 | 162 | ### Logging out 163 | 164 | There are two ways the user can logout: 165 | - By logging out in your app. In this case you SHOULD notify the IdP first so it'll close the global session. 166 | - By logging out of the global SSO Session. In this case the IdP will notify you on `/saml2/{uuid}/slo` endpoint (already provided). 167 | 168 | For the first case, call `Saml2Auth::logout();` or redirect the user to the route `saml.logout` which does just that. 169 | Do not close the session immediately as you need to receive a response confirmation from the IdP (redirection). 170 | That response will be handled by the library at `/saml2/sls` and will fire an event for you to complete the operation. 171 | 172 | For the second case you will only receive the event. Both cases receive the same event. 173 | 174 | Note that for the second case, you may have to manually save your session to make the logout stick (as the session is saved by middleware, but the OneLogin library will redirect back to your IdP before that happens): 175 | 176 | ```php 177 | Event::listen('Slides\Saml2\Events\SignedOut', function (SignedOut $event) { 178 | Auth::logout(); 179 | Session::save(); 180 | }); 181 | ``` 182 | 183 | ### SSO-friendly links 184 | 185 | Sometimes, you need to create links to your application with support of SSO lifecycle. It means you expect a user to be signed in once you click on that link. 186 | 187 | The most popular example is generating links from emails, where you need to make sure when user goes to your application from email, he will be logged in. 188 | To solve this issue, you can use helpers that allow you create SSO-friendly routes and URLs — `saml_url()` and `saml_route()`. 189 | 190 | To generate a link, you need to call one of functions and pass UUID of the tenant as a second parameter, unless your session knows that user was resolved by SSO. 191 | 192 | > To retrieve UUID based on user, you should implement logic that links your internal user to a tenant. 193 | 194 | Then, it generates a link like this: 195 | ``` 196 | https://yourdomain/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/login?returnTo=https://yourdomain.com/your/actual/link 197 | ``` 198 | 199 | Basically, when user clicks on a link, it initiates SSO login process and redirects it back to your needed URL. 200 | 201 | ## Examples 202 | 203 | ### Azure AD 204 | 205 | At this point, we assume you have an application on Azure AD that supports Single Sign On. 206 | 207 | ##### Step 1. Retrieve Identity Provider credentials 208 | 209 | ![Azure AD](https://i.imgur.com/xKLswxB.png) 210 | 211 | You need to retrieve the following parameters: 212 | 213 | - Login URL 214 | - Azure AD Identifier 215 | - Logout URL 216 | - Certificate (Base64) 217 | 218 | ##### Step 2. Create a Tenant 219 | 220 | Based on information you received below, create a Tenant, like this: 221 | 222 | ``` 223 | php artisan saml2:create-tenant \ 224 | --key=azure_testing \ 225 | --entityId=https://sts.windows.net/fb536a7a-7251-4895-a09a-abd8e614c70b/ \ 226 | --loginUrl=https://login.microsoftonline.com/fb536a7a-7251-4895-a09a-abd8e614c70b/saml2 \ 227 | --logoutUrl=https://login.microsoftonline.com/common/wsfederation?wa=wsignout1.0 \ 228 | --x509cert="MIIC0jCCAbqgAw...CapVR4ncDVjvbq+/S" \ 229 | --metadata="customer:11235,anotherfield:value" // you might add some customer parameters here to simplify logging in your customer afterwards 230 | ``` 231 | 232 | Once you successfully created the tenant, you will receive the following output: 233 | 234 | ``` 235 | The tenant #1 (63fffdd1-f416-4bed-b3db-967b6a56896b) was successfully created. 236 | 237 | Credentials for the tenant 238 | -------------------------- 239 | 240 | Identifier (Entity ID): https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/metadata 241 | Reply URL (Assertion Consumer Service URL): https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/acs 242 | Sign on URL: https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/login 243 | Logout URL: https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/logout 244 | Relay State: / (optional) 245 | ``` 246 | 247 | ##### Step 3. Configure Identity Provider 248 | 249 | Using the output below, assign parameters to your IdP on application Single-Sign-On settings page. 250 | 251 | ![Azure AD](https://i.imgur.com/3hkjFLZ.png) 252 | 253 | ##### Step 4. Make sure your application accessible by Azure AD 254 | 255 | Test your application directly from Azure AD and make sure it's accessible worldwide. 256 | 257 | ###### Running locally 258 | 259 | If you want to test it locally, you may use [ngrok](https://ngrok.com/). 260 | 261 | In case if you have a problem with URL creation in your application, you can overwrite host header in your nginx host 262 | config file by adding the following parameters: 263 | 264 | ``` 265 | fastcgi_param HTTP_HOST your.ngrok.io; 266 | fastcgi_param HTTPS on; 267 | ``` 268 | 269 | > Replace `your.ngrok.io` with your actual ngrok URL 270 | 271 | ## Tests 272 | 273 | Run the following in the package folder: 274 | 275 | ``` 276 | vendor/bin/phpunit 277 | ``` 278 | 279 | ## Security 280 | 281 | If you discover any security related issues, please email **brezzhnev@gmail.com** instead of using the issue tracker. 282 | 283 | ## Credits 284 | 285 | - [aacotroneo][link-original-author] 286 | - [brezzhnev][link-author] 287 | - [All Contributors][link-contributors] 288 | 289 | ## License 290 | 291 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 292 | 293 | [ico-version]: https://poser.pugx.org/24slides/laravel-saml2/v/stable?format=flat-square 294 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 295 | [ico-travis]: https://img.shields.io/travis/24Slides/laravel-saml2.svg?style=flat-square 296 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/24slides/laravel-saml2.svg?style=flat-square 297 | [ico-code-coverage]: https://img.shields.io/scrutinizer/coverage/g/24slides/laravel-saml2.svg?style=flat-square 298 | [ico-downloads]: https://img.shields.io/packagist/dt/24slides/laravel-saml2.svg?style=flat-square 299 | 300 | [link-packagist]: https://packagist.org/packages/24slides/laravel-saml2 301 | [link-travis]: https://travis-ci.org/24Slides/laravel-saml2 302 | [link-scrutinizer]: https://scrutinizer-ci.com/g/24slides/laravel-saml2/code-structure 303 | [link-code-quality]: https://scrutinizer-ci.com/g/24slides/laravel-saml2 304 | [link-code-coverage]: https://scrutinizer-ci.com/g/24Slides/laravel-saml2 305 | [link-downloads]: https://packagist.org/packages/24slides/laravel-saml2 306 | [link-original-author]: https://github.com/aacotroneo 307 | [link-author]: https://github.com/brezzhnev 308 | [link-contributors]: ../../contributors 309 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "24slides/laravel-saml2", 3 | "type": "library", 4 | "description": "SAML2 Service Provider integration to your Laravel 5.4+ application, based on OneLogin toolkit", 5 | "keywords": ["laravel", "saml", "saml2", "onelogin", "sso"], 6 | "homepage": "https://github.com/24slides/laravel-saml2", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Artem Brezhnev", 11 | "email": "brezzhnev@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.1", 16 | "ext-openssl": "*", 17 | "illuminate/console": "~5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 18 | "illuminate/database": "~5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 19 | "illuminate/support": "~5.4|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 20 | "onelogin/php-saml": "^3.0|^4.0", 21 | "ramsey/uuid": "^3.8|^4.0" 22 | }, 23 | "require-dev": { 24 | "mockery/mockery": "^0.9.9", 25 | "phpunit/phpunit": "^7.5|^9.0", 26 | "squizlabs/php_codesniffer": "^2.3" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Slides\\Saml2\\": "src/" 31 | }, 32 | "files": [ 33 | "src/helpers.php" 34 | ] 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "aliases": { 39 | "Saml2": "Slides\\Saml2\\Facades\\Auth" 40 | }, 41 | "providers": [ 42 | "Slides\\Saml2\\ServiceProvider" 43 | ] 44 | }, 45 | "branch-aliases": { 46 | "dev-master": "2.0.8-dev" 47 | } 48 | }, 49 | "minimum-stability": "dev", 50 | "prefer-stable": true 51 | } 52 | -------------------------------------------------------------------------------- /config/saml2.php: -------------------------------------------------------------------------------- 1 | \Slides\Saml2\Models\Tenant::class, 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Use built-in routes 19 | |-------------------------------------------------------------------------- 20 | | 21 | | If "useRoutes" set to true, the package defines five new routes: 22 | | 23 | | Method | URI | Name 24 | | -------|---------------------------------|------------------ 25 | | POST | {routesPrefix}/{uuid}/acs | saml.acs 26 | | GET | {routesPrefix}/{uuid}/login | saml.login 27 | | GET | {routesPrefix}/{uuid}/logout | saml.logout 28 | | GET | {routesPrefix}/{uuid}/metadata | saml.metadata 29 | | GET | {routesPrefix}/{uuid}/sls | saml.sls 30 | | 31 | */ 32 | 33 | 'useRoutes' => true, 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Built-in routes prefix 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may define the prefix for built-in routes. 41 | | 42 | */ 43 | 44 | 'routesPrefix' => '/saml2', 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Middle groups to use for the SAML routes 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Note, Laravel 5.2 requires a group which includes StartSession 52 | | 53 | */ 54 | 55 | 'routesMiddleware' => [], 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Signature validation 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Set to true if you want to use parameters from $_SERVER to validate the signature. 63 | | 64 | */ 65 | 66 | 'retrieveParametersFromServer' => false, 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Login redirection URL. 71 | |-------------------------------------------------------------------------- 72 | | 73 | | The redirection URL after successful login. 74 | | 75 | */ 76 | 77 | 'loginRoute' => env('SAML2_LOGIN_URL'), 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Logout redirection URL. 82 | |-------------------------------------------------------------------------- 83 | | 84 | | The redirection URL after successful logout. 85 | | 86 | */ 87 | 88 | 'logoutRoute' => env('SAML2_LOGOUT_URL'), 89 | 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Login error redirection URL. 94 | |-------------------------------------------------------------------------- 95 | | 96 | | The redirection URL after login failing. 97 | | 98 | */ 99 | 100 | 'errorRoute' => env('SAML2_ERROR_URL'), 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Strict mode. 105 | |-------------------------------------------------------------------------- 106 | | 107 | | If 'strict' is True, then the PHP Toolkit will reject unsigned 108 | | or unencrypted messages if it expects them signed or encrypted 109 | | Also will reject the messages if not strictly follow the SAML 110 | | standard: Destination, NameId, Conditions... are validated too. 111 | | 112 | */ 113 | 114 | 'strict' => true, 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Debug mode. 119 | |-------------------------------------------------------------------------- 120 | | 121 | | When enabled, errors must be printed. 122 | | 123 | */ 124 | 125 | 'debug' => env('SAML2_DEBUG', env('APP_DEBUG', false)), 126 | 127 | /* 128 | |-------------------------------------------------------------------------- 129 | | Whether to use `X-Forwarded-*` headers to determine port/domain/protocol. 130 | |-------------------------------------------------------------------------- 131 | | 132 | | If 'proxyVars' is True, then the Saml lib will trust proxy headers 133 | | e.g X-Forwarded-Proto / HTTP_X_FORWARDED_PROTO. This is useful if 134 | | your application is running behind a load balancer which terminates SSL. 135 | | 136 | */ 137 | 138 | 'proxyVars' => false, 139 | 140 | /* 141 | |-------------------------------------------------------------------------- 142 | | Service Provider configuration. 143 | |-------------------------------------------------------------------------- 144 | | 145 | | General setting of the service provider. 146 | | 147 | */ 148 | 149 | 'sp' => [ 150 | 151 | /* 152 | |-------------------------------------------------------------------------- 153 | | NameID format. 154 | |-------------------------------------------------------------------------- 155 | | 156 | | Specifies constraints on the name identifier to be used to 157 | | represent the requested subject. 158 | | 159 | */ 160 | 161 | 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', 162 | 163 | /* 164 | |-------------------------------------------------------------------------- 165 | | SP Certificates. 166 | |-------------------------------------------------------------------------- 167 | | 168 | | Usually x509cert and privateKey of the SP are provided by files placed at 169 | | the certs folder. But we can also provide them with the following parameters. 170 | | 171 | */ 172 | 173 | 'x509cert' => env('SAML2_SP_CERT_x509',''), 174 | 'privateKey' => env('SAML2_SP_CERT_PRIVATEKEY',''), 175 | 176 | /* 177 | |-------------------------------------------------------------------------- 178 | | Identifier (URI) of the SP entity. 179 | |-------------------------------------------------------------------------- 180 | | 181 | | Leave blank to use the 'saml.metadata' route. 182 | | 183 | */ 184 | 185 | 'entityId' => env('SAML2_SP_ENTITYID',''), 186 | 187 | /* 188 | |-------------------------------------------------------------------------- 189 | | The Assertion Consumer Service (ACS) URL. 190 | |-------------------------------------------------------------------------- 191 | | 192 | | URL Location where the from the IdP will be returned, using HTTP-POST binding. 193 | | Leave blank to use the 'saml.acs' route. 194 | | 195 | */ 196 | 197 | 'assertionConsumerService' => [ 198 | 'url' => '', 199 | ], 200 | 201 | /* 202 | |-------------------------------------------------------------------------- 203 | | The Single Logout Service URL. 204 | |-------------------------------------------------------------------------- 205 | | 206 | | Specifies info about where and how the message MUST be 207 | | returned to the requester, in this case our SP. 208 | | 209 | | URL Location where the from the IdP will be returned, using HTTP-Redirect binding. 210 | | Leave blank to use the 'saml.sls' route. 211 | | 212 | */ 213 | 214 | 'singleLogoutService' => [ 215 | 'url' => '' 216 | ], 217 | ], 218 | 219 | /* 220 | |-------------------------------------------------------------------------- 221 | | OneLogin security settings. 222 | |-------------------------------------------------------------------------- 223 | | 224 | | 225 | | 226 | */ 227 | 228 | 'security' => [ 229 | 230 | /* 231 | |-------------------------------------------------------------------------- 232 | | NameId encryption 233 | |-------------------------------------------------------------------------- 234 | | 235 | | Indicates that the nameID of the sent by this SP 236 | | will be encrypted. 237 | | 238 | */ 239 | 240 | 'nameIdEncrypted' => false, 241 | 242 | /* 243 | |-------------------------------------------------------------------------- 244 | | AuthnRequest signage 245 | |-------------------------------------------------------------------------- 246 | | 247 | | Indicates whether the messages sent by 248 | | this SP will be signed. The Metadata of the SP will offer this info 249 | | 250 | */ 251 | 252 | 'authnRequestsSigned' => false, 253 | 254 | /* 255 | |-------------------------------------------------------------------------- 256 | | Logout request signage 257 | |-------------------------------------------------------------------------- 258 | | 259 | | Indicates whether the messages sent by this SP 260 | | will be signed. 261 | | 262 | */ 263 | 264 | 'logoutRequestSigned' => false, 265 | 266 | /* 267 | |-------------------------------------------------------------------------- 268 | | Logout response signage 269 | |-------------------------------------------------------------------------- 270 | | 271 | | Indicates whether the messages sent by this SP 272 | | will be signed. 273 | | 274 | */ 275 | 276 | 'logoutResponseSigned' => false, 277 | 278 | /* 279 | |-------------------------------------------------------------------------- 280 | | Whether need to sign metadata. 281 | |-------------------------------------------------------------------------- 282 | | 283 | | The possible values: 284 | | - false 285 | | - true (use certs) 286 | | - array: 287 | | ``` 288 | | [ 289 | | 'keyFileName' => 'metadata.key', 290 | | 'certFileName' => 'metadata.crt' 291 | | ] 292 | | ``` 293 | | 294 | */ 295 | 296 | 'signMetadata' => false, 297 | 298 | /* 299 | |-------------------------------------------------------------------------- 300 | | Requirement to sign messages. 301 | |-------------------------------------------------------------------------- 302 | | 303 | | Indicates a requirement for the , and 304 | | elements received by this SP to be signed. 305 | | 306 | */ 307 | 308 | 'wantMessagesSigned' => false, 309 | 310 | /* 311 | |-------------------------------------------------------------------------- 312 | | Requirement to sign assertion elements. 313 | |-------------------------------------------------------------------------- 314 | | 315 | | Indicates a requirement for the elements received by 316 | | this SP to be signed. 317 | | 318 | */ 319 | 320 | 'wantAssertionsSigned' => false, 321 | 322 | /* 323 | |-------------------------------------------------------------------------- 324 | | Requirement to encrypt NameID. 325 | |-------------------------------------------------------------------------- 326 | | 327 | | Indicates a requirement for the NameID received by this SP to be encrypted. 328 | | 329 | */ 330 | 331 | 'wantNameIdEncrypted' => false, 332 | 333 | /* 334 | |-------------------------------------------------------------------------- 335 | | Authentication context. 336 | |-------------------------------------------------------------------------- 337 | | 338 | | Set to false and no AuthContext will be sent in the AuthNRequest, 339 | | 340 | | Set true or don't present this parameter and you will get an 341 | | AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' 342 | | 343 | | Set an array with the possible auth context values: 344 | | ['urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'] 345 | | 346 | */ 347 | 348 | 'requestedAuthnContext' => true, 349 | ], 350 | 351 | /* 352 | |-------------------------------------------------------------------------- 353 | | Contact information. 354 | |-------------------------------------------------------------------------- 355 | | 356 | | It is recommended to supply a technical and support contacts. 357 | | 358 | */ 359 | 360 | 'contactPerson' => [ 361 | 'technical' => [ 362 | 'givenName' => env('SAML2_CONTACT_TECHNICAL_NAME', 'name'), 363 | 'emailAddress' => env('SAML2_CONTACT_TECHNICAL_EMAIL', 'no@reply.com') 364 | ], 365 | 'support' => [ 366 | 'givenName' => env('SAML2_CONTACT_SUPPORT_NAME', 'Support'), 367 | 'emailAddress' => env('SAML2_CONTACT_SUPPORT_EMAIL', 'no@reply.com') 368 | ], 369 | ], 370 | 371 | /* 372 | |-------------------------------------------------------------------------- 373 | | Organization information. 374 | |-------------------------------------------------------------------------- 375 | | 376 | | The info in en_US lang is recommended, add more if required. 377 | | 378 | */ 379 | 380 | 'organization' => [ 381 | 'en-US' => [ 382 | 'name' => env('SAML2_ORGANIZATION_NAME', 'Name'), 383 | 'displayname' => env('SAML2_ORGANIZATION_NAME', 'Display Name'), 384 | 'url' => env('SAML2_ORGANIZATION_URL', 'http://url') 385 | ], 386 | ], 387 | 388 | /* 389 | |-------------------------------------------------------------------------- 390 | | Load default migrations 391 | |-------------------------------------------------------------------------- 392 | | 393 | | This will allow you to disable or enable the default migrations of the package. 394 | | 395 | */ 396 | 'load_migrations' => true, 397 | ]; 398 | -------------------------------------------------------------------------------- /database/migrations/2019_06_24_140207_create_saml2_tenants_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->uuid('uuid'); 19 | $table->string('key')->nullable(); 20 | $table->string('idp_entity_id'); 21 | $table->string('idp_login_url'); 22 | $table->string('idp_logout_url'); 23 | $table->text('idp_x509_cert'); 24 | $table->json('metadata'); 25 | $table->timestamps(); 26 | $table->softDeletes(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('saml2_tenants'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2020_10_22_140856_add_relay_state_url_column_to_saml2_tenants_table.php: -------------------------------------------------------------------------------- 1 | string('relay_state_url')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('saml2_tenants', function (Blueprint $table) { 29 | $table->dropColumn('relay_state_url'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2020_10_23_072902_add_name_id_format_column_to_saml2_tenants_table.php: -------------------------------------------------------------------------------- 1 | string('name_id_format')->default('persistent'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('saml2_tenants', function (Blueprint $table) { 29 | $table->dropColumn('name_id_format'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24Slides/laravel-saml2/e7f4938586c65fbe1fdd91552c582cac745223b9/public/.gitkeep -------------------------------------------------------------------------------- /src/Auth.php: -------------------------------------------------------------------------------- 1 | base = $auth; 40 | $this->tenant = $tenant; 41 | } 42 | 43 | /** 44 | * Checks whether a user is authenticated. 45 | * 46 | * @return bool 47 | */ 48 | public function isAuthenticated() 49 | { 50 | return $this->base->isAuthenticated(); 51 | } 52 | 53 | /** 54 | * Create a SAML2 user. 55 | * 56 | * @return Saml2User 57 | */ 58 | public function getSaml2User() 59 | { 60 | return new Saml2User($this->base, $this->tenant); 61 | } 62 | 63 | /** 64 | * The ID of the last message processed. 65 | * 66 | * @return String 67 | */ 68 | public function getLastMessageId() 69 | { 70 | return $this->base->getLastMessageId(); 71 | } 72 | 73 | /** 74 | * Initiate a saml2 login flow. 75 | * 76 | * It will redirect! Before calling this, check if user is 77 | * authenticated (here in saml2). That would be true when the assertion was received this request. 78 | * 79 | * @param string|null $returnTo The target URL the user should be returned to after login. 80 | * @param array $parameters Extra parameters to be added to the GET 81 | * @param bool $forceAuthn When true the AuthNReuqest will set the ForceAuthn='true' 82 | * @param bool $isPassive When true the AuthNReuqest will set the Ispassive='true' 83 | * @param bool $stay True if we want to stay (returns the url string) False to redirect 84 | * @param bool $setNameIdPolicy When true the AuthNReuqest will set a nameIdPolicy element 85 | * 86 | * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters 87 | * 88 | * @throws OneLoginError 89 | */ 90 | public function login( 91 | $returnTo = null, 92 | $parameters = array(), 93 | $forceAuthn = false, 94 | $isPassive = false, 95 | $stay = false, 96 | $setNameIdPolicy = true 97 | ) 98 | { 99 | return $this->base->login($returnTo, $parameters, $forceAuthn, $isPassive, $stay, $setNameIdPolicy); 100 | } 101 | 102 | /** 103 | * Initiate a saml2 logout flow. It will close session on all other SSO services. 104 | * You should close local session if applicable. 105 | * 106 | * @param string|null $returnTo The target URL the user should be returned to after logout. 107 | * @param string|null $nameId The NameID that will be set in the LogoutRequest. 108 | * @param string|null $sessionIndex The SessionIndex (taken from the SAML Response in the SSO process). 109 | * @param string|null $nameIdFormat The NameID Format will be set in the LogoutRequest. 110 | * @param bool $stay True if we want to stay (returns the url string) False to redirect 111 | * @param string|null $nameIdNameQualifier The NameID NameQualifier will be set in the LogoutRequest. 112 | * 113 | * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters 114 | * 115 | * @throws OneLoginError 116 | */ 117 | public function logout( 118 | $returnTo = null, 119 | $nameId = null, 120 | $sessionIndex = null, 121 | $nameIdFormat = null, 122 | $stay = false, 123 | $nameIdNameQualifier = null 124 | ) 125 | { 126 | $auth = $this->base; 127 | 128 | return $auth->logout($returnTo, [], $nameId, $sessionIndex, $stay, $nameIdFormat, $nameIdNameQualifier); 129 | } 130 | 131 | /** 132 | * Process the SAML Response sent by the IdP. 133 | * 134 | * @return array|null 135 | * 136 | * @throws OneLoginError 137 | * @throws \OneLogin\Saml2\ValidationError 138 | */ 139 | public function acs() 140 | { 141 | $this->base->processResponse(); 142 | 143 | $errors = $this->base->getErrors(); 144 | 145 | if (!empty($errors)) { 146 | return $errors; 147 | } 148 | 149 | if (!$this->base->isAuthenticated()) { 150 | return ['error' => 'Could not authenticate']; 151 | } 152 | 153 | return null; 154 | } 155 | 156 | /** 157 | * Process the SAML Logout Response / Logout Request sent by the IdP. 158 | * 159 | * Returns an array with errors if it can not logout. 160 | * 161 | * @param bool $retrieveParametersFromServer 162 | * 163 | * @return array 164 | * 165 | * @throws \OneLogin\Saml2\Error 166 | */ 167 | public function sls($retrieveParametersFromServer = false) 168 | { 169 | $this->base->processSLO(false, null, $retrieveParametersFromServer, function () { 170 | event(new SignedOut()); 171 | }); 172 | 173 | $errors = $this->base->getErrors(); 174 | 175 | return $errors; 176 | } 177 | 178 | /** 179 | * Get metadata about the local SP. Use this to configure your Saml2 IdP. 180 | * 181 | * @return string 182 | * 183 | * @throws \OneLogin\Saml2\Error 184 | * @throws \Exception 185 | * @throws \InvalidArgumentException If metadata is not correctly set 186 | */ 187 | public function getMetadata() 188 | { 189 | $settings = $this->base->getSettings(); 190 | $metadata = $settings->getSPMetadata(); 191 | $errors = $settings->validateMetadata($metadata); 192 | 193 | if (!count($errors)) { 194 | return $metadata; 195 | } 196 | 197 | throw new \InvalidArgumentException( 198 | 'Invalid SP metadata: ' . implode(', ', $errors), 199 | OneLoginError::METADATA_SP_INVALID 200 | ); 201 | } 202 | 203 | /** 204 | * Get the last error reason from \OneLogin_Saml2_Auth, useful for error debugging. 205 | * 206 | * @see \OneLogin_Saml2_Auth::getLastErrorReason() 207 | * 208 | * @return string 209 | */ 210 | public function getLastErrorReason() 211 | { 212 | return $this->base->getLastErrorReason(); 213 | } 214 | 215 | /** 216 | * Get the base authentication handler. 217 | * 218 | * @return OneLoginAuth 219 | */ 220 | public function getBase() 221 | { 222 | return $this->base; 223 | } 224 | 225 | /** 226 | * Set a tenant 227 | * 228 | * @param Tenant $tenant 229 | * 230 | * @return void 231 | */ 232 | public function setTenant(Tenant $tenant) 233 | { 234 | $this->tenant = $tenant; 235 | } 236 | 237 | /** 238 | * Get a resolved tenant. 239 | * 240 | * @return Tenant|null 241 | */ 242 | public function getTenant() 243 | { 244 | return $this->tenant; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/Commands/CreateTenant.php: -------------------------------------------------------------------------------- 1 | tenants = $tenants; 53 | 54 | parent::__construct(); 55 | } 56 | 57 | /** 58 | * Execute the console command. 59 | * 60 | * @return void 61 | */ 62 | public function handle() 63 | { 64 | if (!$entityId = $this->option('entityId')) { 65 | $this->error('Entity ID must be passed as an option --entityId'); 66 | return; 67 | } 68 | 69 | if (!$loginUrl = $this->option('loginUrl')) { 70 | $this->error('Login URL must be passed as an option --loginUrl'); 71 | return; 72 | } 73 | 74 | if (!$logoutUrl = $this->option('logoutUrl')) { 75 | $this->error('Logout URL must be passed as an option --logoutUrl'); 76 | return; 77 | } 78 | 79 | if (!$x509cert = $this->option('x509cert')) { 80 | $this->error('x509 certificate (base64) must be passed as an option --x509cert'); 81 | return; 82 | } 83 | 84 | $key = $this->option('key'); 85 | $metadata = ConsoleHelper::stringToArray($this->option('metadata')); 86 | 87 | if($key && ($tenant = $this->tenants->findByKey($key))) { 88 | $this->renderTenants($tenant, 'Already found tenant(s) using this key'); 89 | $this->error( 90 | 'Cannot create a tenant because the key is already being associated with other tenants.' 91 | . PHP_EOL . 'Firstly, delete tenant(s) or try to create with another with another key.' 92 | ); 93 | 94 | return; 95 | } 96 | 97 | $class = config('saml2.tenantModel', Tenant::class); 98 | $tenant = new $class([ 99 | 'key' => $key, 100 | 'uuid' => \Ramsey\Uuid\Uuid::uuid4(), 101 | 'idp_entity_id' => $entityId, 102 | 'idp_login_url' => $loginUrl, 103 | 'idp_logout_url' => $logoutUrl, 104 | 'idp_x509_cert' => $x509cert, 105 | 'relay_state_url' => $this->option('relayStateUrl'), 106 | 'name_id_format' => $this->resolveNameIdFormat(), 107 | 'metadata' => $metadata, 108 | ]); 109 | 110 | if(!$tenant->save()) { 111 | $this->error('Tenant cannot be saved.'); 112 | return; 113 | } 114 | 115 | $this->info("The tenant #{$tenant->id} ({$tenant->uuid}) was successfully created."); 116 | 117 | $this->renderTenantCredentials($tenant); 118 | 119 | $this->output->newLine(); 120 | } 121 | } -------------------------------------------------------------------------------- /src/Commands/DeleteTenant.php: -------------------------------------------------------------------------------- 1 | tenants = $tenants; 44 | 45 | parent::__construct(); 46 | } 47 | 48 | /** 49 | * Execute the console command. 50 | * 51 | * @return void 52 | */ 53 | public function handle() 54 | { 55 | $tenants = $this->tenants->findByAnyIdentifier($this->argument('tenant'), false); 56 | 57 | if($tenants->isEmpty()) { 58 | $this->error('Cannot find a matching tenant by "' . $this->argument('tenant') . '" identifier'); 59 | return; 60 | } 61 | 62 | $this->renderTenants($tenants, 'Found tenant(s)'); 63 | 64 | if($tenants->count() > 1) { 65 | $deletingId = $this->ask('We have found several tenants, which one would you like to delete? (enter its ID)'); 66 | } 67 | else { 68 | $deletingId = $tenants->first()->id; 69 | } 70 | 71 | $tenant = $tenants->firstWhere('id', $deletingId); 72 | 73 | if($this->option('safe')) { 74 | $tenant->delete(); 75 | 76 | $this->info('The tenant #' . $deletingId . ' safely deleted. To restore it, run:'); 77 | $this->output->block('php artisan saml2:restore-tenant ' . $deletingId); 78 | 79 | return; 80 | } 81 | 82 | if(!$this->confirm('Would you like to forcely delete the tenant #' . $deletingId . '? It cannot be reverted.')) { 83 | return; 84 | } 85 | 86 | $tenant->forceDelete(); 87 | 88 | $this->info('The tenant #' . $deletingId . ' safely deleted.'); 89 | } 90 | } -------------------------------------------------------------------------------- /src/Commands/ListTenants.php: -------------------------------------------------------------------------------- 1 | tenants = $tenants; 43 | 44 | parent::__construct(); 45 | } 46 | 47 | /** 48 | * Execute the console command. 49 | * 50 | * @return void 51 | */ 52 | public function handle() 53 | { 54 | $tenants = $this->tenants->all(); 55 | 56 | if($tenants->isEmpty()) { 57 | $this->info('No tenants found'); 58 | return; 59 | } 60 | 61 | $this->renderTenants($tenants); 62 | } 63 | } -------------------------------------------------------------------------------- /src/Commands/RendersTenants.php: -------------------------------------------------------------------------------- 1 | getTenantColumns($tenant) as $column => $value) { 35 | $columns[] = [$column, $value ?: '(empty)']; 36 | } 37 | 38 | if($tenants->last()->id !== $tenant->id) { 39 | $columns[] = new \Symfony\Component\Console\Helper\TableSeparator(); 40 | } 41 | } 42 | 43 | if($title) { 44 | $this->getOutput()->title($title); 45 | } 46 | 47 | $this->table($headers, $columns); 48 | } 49 | 50 | /** 51 | * Get a columns of the Tenant. 52 | * 53 | * @param \Slides\Saml2\Models\Tenant $tenant 54 | * 55 | * @return array 56 | */ 57 | protected function getTenantColumns(Tenant $tenant) 58 | { 59 | return [ 60 | 'ID' => $tenant->id, 61 | 'UUID' => $tenant->uuid, 62 | 'Key' => $tenant->key, 63 | 'Entity ID' => $tenant->idp_entity_id, 64 | 'Login URL' => $tenant->idp_login_url, 65 | 'Logout URL' => $tenant->idp_logout_url, 66 | 'Relay State URL' => $tenant->relay_state_url, 67 | 'Name ID format' => $tenant->name_id_format, 68 | 'x509 cert' => Str::limit($tenant->idp_x509_cert, 50), 69 | 'Metadata' => $this->renderArray($tenant->metadata ?: []), 70 | 'Created' => $tenant->created_at->toDateTimeString(), 71 | 'Updated' => $tenant->updated_at->toDateTimeString(), 72 | 'Deleted' => $tenant->deleted_at ? $tenant->deleted_at->toDateTimeString() : null 73 | ]; 74 | } 75 | 76 | /** 77 | * Render a tenant credentials. 78 | * 79 | * @param \Slides\Saml2\Models\Tenant $tenant 80 | * 81 | * @return void 82 | */ 83 | protected function renderTenantCredentials(Tenant $tenant) 84 | { 85 | $this->output->section('Credentials for the tenant'); 86 | 87 | $this->getOutput()->text([ 88 | 'Identifier (Entity ID): ' . route('saml.metadata', ['uuid' => $tenant->uuid]) . '', 89 | 'Reply URL (Assertion Consumer Service URL): ' . route('saml.acs', ['uuid' => $tenant->uuid]) . '', 90 | 'Sign on URL: ' . route('saml.login', ['uuid' => $tenant->uuid]) . '', 91 | 'Logout URL: ' . route('saml.logout', ['uuid' => $tenant->uuid]) . '', 92 | 'Relay State: ' . ($tenant->relay_state_url ?: config('saml2.loginRoute')) . ' (optional)' 93 | ]); 94 | } 95 | 96 | /** 97 | * Print an array to a string. 98 | * 99 | * @param array $array 100 | * 101 | * @return string 102 | */ 103 | protected function renderArray(array $array) 104 | { 105 | $lines = []; 106 | 107 | foreach ($array as $key => $value) { 108 | $lines[] = "$key: $value"; 109 | } 110 | 111 | return implode(PHP_EOL, $lines); 112 | } 113 | } -------------------------------------------------------------------------------- /src/Commands/RestoreTenant.php: -------------------------------------------------------------------------------- 1 | tenants = $tenants; 43 | 44 | parent::__construct(); 45 | } 46 | 47 | /** 48 | * Execute the console command. 49 | * 50 | * @return void 51 | */ 52 | public function handle() 53 | { 54 | if(!$tenant = $this->tenants->findById($this->argument('id'))) { 55 | $this->error('Cannot find a tenant #' . $this->argument('id')); 56 | return; 57 | } 58 | 59 | $this->renderTenants($tenant, 'Found a deleted tenant'); 60 | 61 | if(!$this->confirm('Would you like to restore it?')) { 62 | return; 63 | } 64 | 65 | $tenant->restore(); 66 | 67 | $this->info('The tenant #' . $tenant->id . ' successfully restored.'); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Commands/TenantCredentials.php: -------------------------------------------------------------------------------- 1 | tenants = $tenants; 44 | 45 | parent::__construct(); 46 | } 47 | 48 | /** 49 | * Execute the console command. 50 | * 51 | * @return void 52 | */ 53 | public function handle() 54 | { 55 | if(!$tenant = $this->tenants->findById($this->argument('id'))) { 56 | $this->error('Cannot find a tenant #' . $this->argument('id')); 57 | return; 58 | } 59 | 60 | $this->renderTenants($tenant, 'The tenant model'); 61 | $this->renderTenantCredentials($tenant); 62 | 63 | $this->output->newLine(); 64 | } 65 | } -------------------------------------------------------------------------------- /src/Commands/UpdateTenant.php: -------------------------------------------------------------------------------- 1 | tenants = $tenants; 52 | 53 | parent::__construct(); 54 | } 55 | 56 | /** 57 | * Execute the console command. 58 | * 59 | * @return void 60 | */ 61 | public function handle() 62 | { 63 | if(!$tenant = $this->tenants->findById($this->argument('id'))) { 64 | $this->error('Cannot find a tenant #' . $this->argument('id')); 65 | return; 66 | } 67 | 68 | $tenant->update(array_filter([ 69 | 'key' => $this->option('key'), 70 | 'idp_entity_id' => $this->option('entityId'), 71 | 'idp_login_url' => $this->option('loginUrl'), 72 | 'idp_logout_url' => $this->option('logoutUrl'), 73 | 'idp_x509_cert' => $this->option('x509cert'), 74 | 'relay_state_url' => $this->option('relayStateUrl'), 75 | 'name_id_format' => $this->resolveNameIdFormat(), 76 | 'metadata' => ConsoleHelper::stringToArray($this->option('metadata')) 77 | ])); 78 | 79 | if(!$tenant->save()) { 80 | $this->error('Tenant cannot be saved.'); 81 | return; 82 | } 83 | 84 | $this->info("The tenant #{$tenant->id} ({$tenant->uuid}) was successfully updated."); 85 | 86 | $this->renderTenantCredentials($tenant); 87 | 88 | $this->output->newLine(); 89 | } 90 | } -------------------------------------------------------------------------------- /src/Commands/ValidatesInput.php: -------------------------------------------------------------------------------- 1 | option($option) ?: 'persistent'; 22 | 23 | if ($this->validateNameIdFormat($value)) { 24 | return $value; 25 | } 26 | 27 | $this->error('Name ID format is invalid. Supported values: ' . implode(', ', $this->supportedNameIdFormats())); 28 | 29 | return null; 30 | } 31 | 32 | /** 33 | * Validate Name ID format. 34 | * 35 | * @param string $format 36 | * 37 | * @return bool 38 | */ 39 | protected function validateNameIdFormat(string $format): bool 40 | { 41 | return in_array($format, $this->supportedNameIdFormats()); 42 | } 43 | 44 | /** 45 | * The list of supported Name ID formats. 46 | * 47 | * See https://docs.oracle.com/cd/E19316-01/820-3886/6nfcvtepi/index.html 48 | * 49 | * @return string[]|array 50 | */ 51 | protected function supportedNameIdFormats(): array 52 | { 53 | return [ 54 | 'persistent', 55 | 'transient', 56 | 'emailAddress', 57 | 'unspecified', 58 | 'X509SubjectName', 59 | 'WindowsDomainQualifiedName', 60 | 'kerberos', 61 | 'entity' 62 | ]; 63 | } 64 | } -------------------------------------------------------------------------------- /src/Events/SignedIn.php: -------------------------------------------------------------------------------- 1 | user = $user; 38 | $this->auth = $auth; 39 | } 40 | 41 | /** 42 | * Get the authentication handler for a SAML sign in attempt 43 | * 44 | * @return Auth The authentication handler for the SignedIn event 45 | */ 46 | public function getAuth(): Auth 47 | { 48 | return $this->auth; 49 | } 50 | 51 | /** 52 | * Get the user represented in the SAML sign in attempt 53 | * 54 | * @return Saml2User The user for the SignedIn event 55 | */ 56 | public function getSaml2User(): Saml2User 57 | { 58 | return $this->user; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Events/SignedOut.php: -------------------------------------------------------------------------------- 1 | $item) { 35 | $item = explode($valueDelimiter, $item); 36 | 37 | $key = Arr::get($item, 0); 38 | $value = Arr::get($item, 1); 39 | 40 | if(is_null($value)) { 41 | $value = $key; 42 | $key = $index; 43 | } 44 | 45 | $values[trim($key)] = trim($value); 46 | } 47 | 48 | return $values; 49 | } 50 | 51 | /** 52 | * Converts an array to string 53 | * 54 | * ['one', 'two', 'three'] to 'one, two, three', 55 | * ['one' => 1, 'two' => 2, 'three' => 3] to 'one:1,two:2,three:3' 56 | * 57 | * @param array $array 58 | * 59 | * @return string 60 | */ 61 | public static function arrayToString(array $array): string 62 | { 63 | $values = []; 64 | 65 | foreach ($array as $key => $value) { 66 | if(is_array($value)) { 67 | continue; 68 | } 69 | 70 | $values[] = is_string($key) 71 | ? $key . ':' . $value 72 | : $value; 73 | } 74 | 75 | return implode(',', $values); 76 | } 77 | } -------------------------------------------------------------------------------- /src/Http/Controllers/Saml2Controller.php: -------------------------------------------------------------------------------- 1 | getMetadata(); 30 | 31 | return response($metadata, 200, ['Content-Type' => 'text/xml']); 32 | } 33 | 34 | /** 35 | * Process the SAML Response sent by the IdP. 36 | * 37 | * Fires "SignedIn" event if a valid user is found. 38 | * 39 | * @param Auth $auth 40 | * 41 | * @return \Illuminate\Support\Facades\Redirect 42 | * 43 | * @throws OneLoginError 44 | * @throws \OneLogin\Saml2\ValidationError 45 | */ 46 | public function acs(Auth $auth) 47 | { 48 | $errors = $auth->acs(); 49 | 50 | if (!empty($errors)) { 51 | $error = $auth->getLastErrorReason(); 52 | $uuid = $auth->getTenant()->uuid; 53 | 54 | logger()->error('saml2.error_detail', compact('uuid', 'error')); 55 | session()->flash('saml2.error_detail', [$error]); 56 | 57 | logger()->error('saml2.error', $errors); 58 | session()->flash('saml2.error', $errors); 59 | 60 | return redirect(config('saml2.errorRoute')); 61 | } 62 | 63 | $user = $auth->getSaml2User(); 64 | 65 | event(new SignedIn($user, $auth)); 66 | 67 | $redirectUrl = $user->getIntendedUrl(); 68 | 69 | if ($redirectUrl) { 70 | return redirect($redirectUrl); 71 | } 72 | 73 | return redirect($auth->getTenant()->relay_state_url ?: config('saml2.loginRoute')); 74 | } 75 | 76 | /** 77 | * Process the SAML Logout Response / Logout Request sent by the IdP. 78 | * 79 | * Fires 'saml2.logoutRequestReceived' event if its valid. 80 | * 81 | * This means the user logged out of the SSO infrastructure, you 'should' log him out locally too. 82 | * 83 | * @param Auth $auth 84 | * 85 | * @return \Illuminate\Support\Facades\Redirect 86 | * 87 | * @throws OneLoginError 88 | * @throws \Exception 89 | */ 90 | public function sls(Auth $auth) 91 | { 92 | $errors = $auth->sls(config('saml2.retrieveParametersFromServer')); 93 | 94 | if (!empty($errors)) { 95 | $error = $auth->getLastErrorReason(); 96 | $uuid = $auth->getTenant()->uuid; 97 | 98 | logger()->error('saml2.error_detail', compact('uuid', 'error')); 99 | session()->flash('saml2.error_detail', [$error]); 100 | 101 | logger()->error('saml2.error', $errors); 102 | session()->flash('saml2.error', $errors); 103 | 104 | return redirect(config('saml2.errorRoute')); 105 | } 106 | 107 | return redirect(config('saml2.logoutRoute')); //may be set a configurable default 108 | } 109 | 110 | /** 111 | * Initiate a login request. 112 | * 113 | * @param Illuminate\Http\Request $request 114 | * @param Auth $auth 115 | * 116 | * @return void 117 | * 118 | * @throws OneLoginError 119 | */ 120 | public function login(Request $request, Auth $auth) 121 | { 122 | $redirectUrl = $auth->getTenant()->relay_state_url ?: config('saml2.loginRoute'); 123 | 124 | $auth->login($request->query('returnTo', $redirectUrl)); 125 | } 126 | 127 | /** 128 | * Initiate a logout request. 129 | * 130 | * @param Illuminate\Http\Request $request 131 | * @param Auth $auth 132 | * 133 | * @return void 134 | * 135 | * @throws OneLoginError 136 | */ 137 | public function logout(Request $request, Auth $auth) 138 | { 139 | $auth->logout( 140 | $request->query('returnTo'), 141 | $request->query('nameId'), 142 | $request->query('sessionIndex') 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Http/Middleware/ResolveTenant.php: -------------------------------------------------------------------------------- 1 | tenants = $tenants; 36 | $this->builder = $builder; 37 | } 38 | 39 | /** 40 | * Handle an incoming request. 41 | * 42 | * @param \Illuminate\Http\Request $request 43 | * @param \Closure $next 44 | * 45 | * @throws NotFoundHttpException 46 | * 47 | * @return mixed 48 | */ 49 | public function handle($request, \Closure $next) 50 | { 51 | if(!$tenant = $this->resolveTenant($request)) { 52 | throw new NotFoundHttpException(); 53 | } 54 | 55 | if (config('saml2.debug')) { 56 | Log::debug('[Saml2] Tenant resolved', [ 57 | 'uuid' => $tenant->uuid, 58 | 'id' => $tenant->id, 59 | 'key' => $tenant->key 60 | ]); 61 | } 62 | 63 | session()->flash('saml2.tenant.uuid', $tenant->uuid); 64 | 65 | $this->builder 66 | ->withTenant($tenant) 67 | ->bootstrap(); 68 | 69 | return $next($request); 70 | } 71 | 72 | /** 73 | * Resolve a tenant by a request. 74 | * 75 | * @param \Illuminate\Http\Request $request 76 | * 77 | * @return \Slides\Saml2\Models\Tenant|null 78 | */ 79 | protected function resolveTenant($request) 80 | { 81 | if(!$uuid = $request->route('uuid')) { 82 | if (config('saml2.debug')) { 83 | Log::debug('[Saml2] Tenant UUID is not present in the URL so cannot be resolved', [ 84 | 'url' => $request->fullUrl() 85 | ]); 86 | } 87 | 88 | return null; 89 | } 90 | 91 | if(!$tenant = $this->tenants->findByUUID($uuid)) { 92 | if (config('saml2.debug')) { 93 | Log::debug('[Saml2] Tenant doesn\'t exist', [ 94 | 'uuid' => $uuid 95 | ]); 96 | } 97 | 98 | return null; 99 | } 100 | 101 | if($tenant->trashed()) { 102 | if (config('saml2.debug')) { 103 | Log::debug('[Saml2] Tenant #' . $tenant->id. ' resolved but marked as deleted', [ 104 | 'id' => $tenant->id, 105 | 'uuid' => $uuid, 106 | 'deleted_at' => $tenant->deleted_at->toDateTimeString() 107 | ]); 108 | } 109 | 110 | return null; 111 | } 112 | 113 | return $tenant; 114 | } 115 | } -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | config('saml2.routesPrefix'), 7 | 'middleware' => array_merge(['saml2.resolveTenant'], config('saml2.routesMiddleware')), 8 | ], function () { 9 | Route::get('/{uuid}/logout', array( 10 | 'as' => 'saml.logout', 11 | 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@logout', 12 | )); 13 | 14 | Route::get('/{uuid}/login', array( 15 | 'as' => 'saml.login', 16 | 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@login', 17 | )); 18 | 19 | Route::get('/{uuid}/metadata', array( 20 | 'as' => 'saml.metadata', 21 | 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@metadata', 22 | )); 23 | 24 | Route::post('/{uuid}/acs', array( 25 | 'as' => 'saml.acs', 26 | 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@acs', 27 | )); 28 | 29 | Route::get('/{uuid}/sls', array( 30 | 'as' => 'saml.sls', 31 | 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@sls', 32 | )); 33 | }); 34 | -------------------------------------------------------------------------------- /src/Models/Tenant.php: -------------------------------------------------------------------------------- 1 | 'array' 62 | ]; 63 | } 64 | -------------------------------------------------------------------------------- /src/OneLoginBuilder.php: -------------------------------------------------------------------------------- 1 | app = $app; 39 | } 40 | 41 | /** 42 | * Set a tenant. 43 | * 44 | * @param Tenant $tenant 45 | * 46 | * @return $this 47 | */ 48 | public function withTenant(Tenant $tenant) 49 | { 50 | $this->tenant = $tenant; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Bootstrap the OneLogin toolkit. 57 | * 58 | * @param Tenant $tenant 59 | * 60 | * @return void 61 | */ 62 | public function bootstrap() 63 | { 64 | if ($this->app['config']->get('saml2.proxyVars', false)) { 65 | OneLoginUtils::setProxyVars(true); 66 | } 67 | 68 | $this->app->singleton('OneLogin_Saml2_Auth', function ($app) { 69 | $config = $app['config']['saml2']; 70 | 71 | $this->setConfigDefaultValues($config); 72 | 73 | $oneLoginConfig = $config; 74 | $oneLoginConfig['idp'] = [ 75 | 'entityId' => $this->tenant->idp_entity_id, 76 | 'singleSignOnService' => ['url' => $this->tenant->idp_login_url], 77 | 'singleLogoutService' => ['url' => $this->tenant->idp_logout_url], 78 | 'x509cert' => $this->tenant->idp_x509_cert 79 | ]; 80 | 81 | $oneLoginConfig['sp']['NameIDFormat'] = $this->resolveNameIdFormatPrefix($this->tenant->name_id_format); 82 | 83 | return new OneLoginAuth($oneLoginConfig); 84 | }); 85 | 86 | $this->app->singleton('Slides\Saml2\Auth', function ($app) { 87 | return new \Slides\Saml2\Auth($app['OneLogin_Saml2_Auth'], $this->tenant); 88 | }); 89 | } 90 | 91 | /** 92 | * Set default config values if they weren't set. 93 | * 94 | * @param array $config 95 | * 96 | * @return void 97 | */ 98 | protected function setConfigDefaultValues(array &$config) 99 | { 100 | foreach ($this->configDefaultValues() as $key => $default) { 101 | if(!Arr::get($config, $key)) { 102 | Arr::set($config, $key, $default); 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Configuration default values that must be replaced with custom ones. 109 | * 110 | * @return array 111 | */ 112 | protected function configDefaultValues() 113 | { 114 | return [ 115 | 'sp.entityId' => URL::route('saml.metadata', ['uuid' => $this->tenant->uuid]), 116 | 'sp.assertionConsumerService.url' => URL::route('saml.acs', ['uuid' => $this->tenant->uuid]), 117 | 'sp.singleLogoutService.url' => URL::route('saml.sls', ['uuid' => $this->tenant->uuid]) 118 | ]; 119 | } 120 | 121 | /** 122 | * Resolve the Name ID Format prefix. 123 | * 124 | * @param string $format 125 | * 126 | * @return string 127 | */ 128 | protected function resolveNameIdFormatPrefix(string $format): string 129 | { 130 | switch ($format) { 131 | case 'emailAddress': 132 | case 'X509SubjectName': 133 | case 'WindowsDomainQualifiedName': 134 | case 'unspecified': 135 | return 'urn:oasis:names:tc:SAML:1.1:nameid-format:' . $format; 136 | default: 137 | return 'urn:oasis:names:tc:SAML:2.0:nameid-format:'. $format; 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /src/Repositories/TenantRepository.php: -------------------------------------------------------------------------------- 1 | withTrashed(); 28 | } 29 | 30 | return $query; 31 | } 32 | 33 | /** 34 | * Find all tenants. 35 | * 36 | * @param bool $withTrashed Whether need to include safely deleted records. 37 | * 38 | * @return Tenant[]|\Illuminate\Database\Eloquent\Collection 39 | */ 40 | public function all(bool $withTrashed = true) 41 | { 42 | return $this->query($withTrashed)->get(); 43 | } 44 | 45 | /** 46 | * Find a tenant by any identifier. 47 | * 48 | * @param int|string $key ID, key or UUID 49 | * @param bool $withTrashed Whether need to include safely deleted records. 50 | * 51 | * @return Tenant[]|\Illuminate\Database\Eloquent\Collection 52 | */ 53 | public function findByAnyIdentifier($key, bool $withTrashed = true) 54 | { 55 | $query = $this->query($withTrashed); 56 | 57 | if (is_int($key)) { 58 | return $query->where('id', $key)->get(); 59 | } 60 | 61 | return $query->where('key', $key) 62 | ->orWhere('uuid', $key) 63 | ->get(); 64 | } 65 | 66 | /** 67 | * Find a tenant by the key. 68 | * 69 | * @param string $key 70 | * @param bool $withTrashed 71 | * 72 | * @return Tenant|\Illuminate\Database\Eloquent\Model|null 73 | */ 74 | public function findByKey(string $key, bool $withTrashed = true) 75 | { 76 | return $this->query($withTrashed) 77 | ->where('key', $key) 78 | ->first(); 79 | } 80 | 81 | /** 82 | * Find a tenant by ID. 83 | * 84 | * @param int $id 85 | * @param bool $withTrashed 86 | * 87 | * @return Tenant|\Illuminate\Database\Eloquent\Model|null 88 | */ 89 | public function findById(int $id, bool $withTrashed = true) 90 | { 91 | return $this->query($withTrashed) 92 | ->where('id', $id) 93 | ->first(); 94 | } 95 | 96 | /** 97 | * Find a tenant by UUID. 98 | * 99 | * @param int $uuid 100 | * @param bool $withTrashed 101 | * 102 | * @return Tenant|\Illuminate\Database\Eloquent\Model|null 103 | */ 104 | public function findByUUID(string $uuid, bool $withTrashed = true) 105 | { 106 | return $this->query($withTrashed) 107 | ->where('uuid', $uuid) 108 | ->first(); 109 | } 110 | } -------------------------------------------------------------------------------- /src/Saml2User.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 38 | $this->tenant = $tenant; 39 | } 40 | 41 | /** 42 | * Get the user ID retrieved from assertion processed this request. 43 | * 44 | * @return string 45 | */ 46 | public function getUserId() 47 | { 48 | return $this->auth->getNameId(); 49 | } 50 | 51 | /** 52 | * Get the attributes retrieved from assertion processed this request 53 | * 54 | * @return array 55 | */ 56 | public function getAttributes() 57 | { 58 | return $this->auth->getAttributes(); 59 | } 60 | 61 | /** 62 | * Returns the requested SAML attribute 63 | * 64 | * @param string $name The requested attribute of the user. 65 | * 66 | * @return array|null Requested SAML attribute ($name). 67 | */ 68 | public function getAttribute($name) 69 | { 70 | return $this->auth->getAttribute($name); 71 | } 72 | 73 | /** 74 | * The attributes retrieved from assertion processed this request. 75 | * 76 | * @return array 77 | */ 78 | public function getAttributesWithFriendlyName() 79 | { 80 | return $this->auth->getAttributesWithFriendlyName(); 81 | } 82 | 83 | /** 84 | * The SAML assertion processed this request. 85 | * 86 | * @return string 87 | */ 88 | public function getRawSamlAssertion() 89 | { 90 | return app('request')->input('SAMLResponse'); //just this request 91 | } 92 | 93 | /** 94 | * Get the intended URL. 95 | * 96 | * @return mixed 97 | */ 98 | public function getIntendedUrl() 99 | { 100 | $relayState = app('request')->input('RelayState'); 101 | 102 | $url = app('Illuminate\Contracts\Routing\UrlGenerator'); 103 | 104 | if ($relayState && $url->full() != $relayState) { 105 | return $relayState; 106 | } 107 | 108 | return null; 109 | } 110 | 111 | /** 112 | * Parses a SAML property and adds this property to this user or returns the value. 113 | * 114 | * @param string $samlAttribute 115 | * @param string $propertyName 116 | * 117 | * @return array|null 118 | */ 119 | public function parseUserAttribute($samlAttribute = null, $propertyName = null) 120 | { 121 | if(empty($samlAttribute)) { 122 | return null; 123 | } 124 | 125 | if(empty($propertyName)) { 126 | return $this->getAttribute($samlAttribute); 127 | } 128 | 129 | return $this->{$propertyName} = $this->getAttribute($samlAttribute); 130 | } 131 | 132 | /** 133 | * Parse the SAML attributes and add them to this user. 134 | * 135 | * @param array $attributes Array of properties which need to be parsed, like ['email' => 'urn:oid:0.9.2342.19200300.100.1.3'] 136 | * 137 | * @return void 138 | */ 139 | public function parseAttributes($attributes = []) 140 | { 141 | foreach($attributes as $propertyName => $samlAttribute) { 142 | $this->parseUserAttribute($samlAttribute, $propertyName); 143 | } 144 | } 145 | 146 | /** 147 | * Get user's session index. 148 | * 149 | * @return null|string 150 | */ 151 | public function getSessionIndex() 152 | { 153 | return $this->auth->getSessionIndex(); 154 | } 155 | 156 | /** 157 | * Get user's name ID. 158 | * 159 | * @return string 160 | */ 161 | public function getNameId() 162 | { 163 | return $this->auth->getNameId(); 164 | } 165 | 166 | /** 167 | * Set a tenant 168 | * 169 | * @param Tenant $tenant 170 | * 171 | * @return void 172 | */ 173 | public function setTenant(Tenant $tenant) 174 | { 175 | $this->tenant = $tenant; 176 | } 177 | 178 | /** 179 | * Get a resolved tenant. 180 | * 181 | * @return Tenant|null 182 | */ 183 | public function getTenant() 184 | { 185 | return $this->tenant; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | bootMiddleware(); 27 | $this->bootRoutes(); 28 | $this->bootPublishes(); 29 | $this->bootCommands(); 30 | $this->loadMigrations(); 31 | } 32 | 33 | /** 34 | * Bootstrap the routes. 35 | * 36 | * @return void 37 | */ 38 | protected function bootRoutes() 39 | { 40 | if($this->app['config']['saml2.useRoutes'] == true) { 41 | include __DIR__ . '/Http/routes.php'; 42 | } 43 | } 44 | 45 | /** 46 | * Bootstrap the publishable files. 47 | * 48 | * @return void 49 | */ 50 | protected function bootPublishes() 51 | { 52 | $source = __DIR__ . '/../config/saml2.php'; 53 | 54 | $this->publishes([$source => config_path('saml2.php')]); 55 | $this->mergeConfigFrom($source, 'saml2'); 56 | } 57 | 58 | /** 59 | * Bootstrap the console commands. 60 | * 61 | * @return void 62 | */ 63 | protected function bootCommands() 64 | { 65 | $this->commands([ 66 | \Slides\Saml2\Commands\CreateTenant::class, 67 | \Slides\Saml2\Commands\UpdateTenant::class, 68 | \Slides\Saml2\Commands\DeleteTenant::class, 69 | \Slides\Saml2\Commands\RestoreTenant::class, 70 | \Slides\Saml2\Commands\ListTenants::class, 71 | \Slides\Saml2\Commands\TenantCredentials::class 72 | ]); 73 | } 74 | 75 | /** 76 | * Bootstrap the console commands. 77 | * 78 | * @return void 79 | */ 80 | protected function bootMiddleware() 81 | { 82 | $this->app['router']->aliasMiddleware('saml2.resolveTenant', \Slides\Saml2\Http\Middleware\ResolveTenant::class); 83 | } 84 | 85 | /** 86 | * Load the package migrations. 87 | * 88 | * @return void 89 | */ 90 | protected function loadMigrations() 91 | { 92 | if (config('saml2.load_migrations', true)) { 93 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 94 | } 95 | } 96 | 97 | /** 98 | * Get the services provided by the provider. 99 | * 100 | * @return array 101 | */ 102 | public function provides() 103 | { 104 | return []; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | $uuid, 'returnTo' => $target]); 26 | } 27 | } 28 | 29 | if (!function_exists('saml_route')) 30 | { 31 | /** 32 | * Generate a URL to saml/{uuid}/login which redirects to a target route. 33 | * 34 | * @param string $name 35 | * @param string|null $uuid A tenant UUID. 36 | * @param array $parameters 37 | * 38 | * @return string 39 | */ 40 | function saml_route(string $name, ?string $uuid = null, $parameters = []) 41 | { 42 | $target = \Illuminate\Support\Facades\URL::route($name, $parameters, true); 43 | 44 | if(!$uuid) { 45 | if(!$uuid = saml_tenant_uuid()) { 46 | return $target; 47 | } 48 | } 49 | 50 | return \Illuminate\Support\Facades\URL::route('saml.login', ['uuid' => $uuid, 'returnTo' => $target]); 51 | } 52 | } 53 | 54 | if (!function_exists('saml_tenant_uuid')) 55 | { 56 | /** 57 | * Get a resolved Tenant UUID based on current URL. 58 | * 59 | * @return string|null 60 | */ 61 | function saml_tenant_uuid() 62 | { 63 | return session()->get('saml2.tenant.uuid'); 64 | } 65 | } --------------------------------------------------------------------------------