Code Monkey home page Code Monkey logo

laravel-saml2's Introduction

[Laravel 5.4+] SAML Service Provider

Latest Stable Version Software License Build Status Quality Score Code Coverage Total Downloads

An integration to add SSO to your service via SAML2 protocol based on OneLogin toolkit.

This package turns your application into Service Provider with the support of multiple Identity Providers.

Requirements

  • Laravel 5.4+
  • PHP 7.0+

Getting Started

Installing

Step 1. Install dependency
composer require 24slides/laravel-saml2

If you are using Laravel 5.5 and higher, the service provider will be automatically registered.

For older versions, you have to add the service provider and alias to your config/app.php:

'providers' => [
    ...
    Slides\Saml2\ServiceProvider::class,
]

'alias' => [
    ...
    'Saml2' => Slides\Saml2\Facades\Auth::class,
]
Step 2. Publish the configuration file.
php artisan vendor:publish --provider="Slides\Saml2\ServiceProvider"
Step 3. Run migrations
php artisan migrate

Configuring

Once you publish saml2.php to app/config, you need to configure your SP. Most of options are inherited from OneLogin Toolkit, so you can check documentation there.

Identity Providers (IdPs)

To distinguish between identity providers there is an entity called Tenant that represent each IdP.

When request comes to an application, the middleware parses UUID and resolves the Tenant.

You can easily manage tenants using the following console commands:

  • artisan saml2:create-tenant
  • artisan saml2:update-tenant
  • artisan saml2:delete-tenant
  • artisan saml2:restore-tenant
  • artisan saml2:list-tenants
  • artisan saml2:tenant-credentials

To learn their options, run a command with -h parameter.

Each Tenant has the following attributes:

  • UUID — a unique identifier that allows to resolve a tenannt and configure SP correspondingly
  • Key — a custom key to use for application needs
  • Entity IDIdentity Provider Entity ID
  • Login URL — Identity Provider Single Sign On URL
  • Logout URL — Identity Provider Logout URL
  • x509 certificate — The certificate provided by Identity Provider in base64 format
  • Metadata — Custom parameters for your application needs

Default routes

The following routes are registered by default:

  • GET saml2/{uuid}/login
  • GET saml2/{uuid}/logout
  • GET saml2/{uuid}/metadata
  • POST saml2/{uuid}/acs
  • POST saml2/{uuid}/sls

You may disable them by setting saml2.useRoutes to false.

/saml2 prefix can be changed via saml2.routesPrefix config parameter.

Usage

Authentication events

The simplest way to handle SAML authentication is to add listeners on Slides\Saml2\SignedIn and Slides\Saml2\SignedOut events.

Event::listen(\Slides\Saml2\Events\SignedIn::class, function (\Slides\Saml2\Events\SignedIn $event) {
    $messageId = $event->getAuth()->getLastMessageId();
    
    // your own code preventing reuse of a $messageId to stop replay attacks
    $samlUser = $event->getSaml2User();
    
    $userData = [
        'id' => $samlUser->getUserId(),
        'attributes' => $samlUser->getAttributes(),
        'assertion' => $samlUser->getRawSamlAssertion()
    ];
    
    $user = // find user by ID or attribute
    
    // Login a user.
    Auth::login($user);
});

Middleware

To define a middleware for default routes, add its name to config/saml2.php:

/*
|--------------------------------------------------------------------------
| Built-in routes prefix
|--------------------------------------------------------------------------
|
| Here you may define the prefix for built-in routes.
|
*/

'routesMiddleware' => ['saml'],

Then you need to define necessary middlewares for your group in app/Http/Kernel.php:

protected $middlewareGroups = [
    'web' => [
        ...
    ],
    'api' => [
        ...
    ],
    'saml' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
    ],

Logging out

There are two ways the user can logout:

  • By logging out in your app. In this case you SHOULD notify the IdP first so it'll close the global session.
  • By logging out of the global SSO Session. In this case the IdP will notify you on /saml2/{uuid}/slo endpoint (already provided).

For the first case, call Saml2Auth::logout(); or redirect the user to the route saml.logout which does just that. Do not close the session immediately as you need to receive a response confirmation from the IdP (redirection). That response will be handled by the library at /saml2/sls and will fire an event for you to complete the operation.

For the second case you will only receive the event. Both cases receive the same event.

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):

Event::listen('Slides\Saml2\Events\SignedOut', function (SignedOut $event) {
    Auth::logout();
    Session::save();
});

SSO-friendly links

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.

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. To solve this issue, you can use helpers that allow you create SSO-friendly routes and URLs — saml_url() and saml_route().

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.

To retrieve UUID based on user, you should implement logic that links your internal user to a tenant.

Then, it generates a link like this:

https://yourdomain/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/login?returnTo=https://yourdomain.com/your/actual/link

Basically, when user clicks on a link, it initiates SSO login process and redirects it back to your needed URL.

Examples

Azure AD

At this point, we assume you have an application on Azure AD that supports Single Sign On.

Step 1. Retrieve Identity Provider credentials

Azure AD

You need to retrieve the following parameters:

  • Login URL
  • Azure AD Identifier
  • Logout URL
  • Certificate (Base64)
Step 2. Create a Tenant

Based on information you received below, create a Tenant, like this:

php artisan saml2:create-tenant \
  --key=azure_testing \
  --entityId=https://sts.windows.net/fb536a7a-7251-4895-a09a-abd8e614c70b/ \
  --loginUrl=https://login.microsoftonline.com/fb536a7a-7251-4895-a09a-abd8e614c70b/saml2 \
  --logoutUrl=https://login.microsoftonline.com/common/wsfederation?wa=wsignout1.0 \
  --x509cert="MIIC0jCCAbqgAw...CapVR4ncDVjvbq+/S" \
  --metadata="customer:11235,anotherfield:value" // you might add some customer parameters here to simplify logging in your customer afterwards

Once you successfully created the tenant, you will receive the following output:

The tenant #1 (63fffdd1-f416-4bed-b3db-967b6a56896b) was successfully created.

Credentials for the tenant
--------------------------

 Identifier (Entity ID): https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/metadata
 Reply URL (Assertion Consumer Service URL): https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/acs
 Sign on URL: https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/login
 Logout URL: https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/logout
 Relay State: / (optional)
Step 3. Configure Identity Provider

Using the output below, assign parameters to your IdP on application Single-Sign-On settings page.

Azure AD

Step 4. Make sure your application accessible by Azure AD

Test your application directly from Azure AD and make sure it's accessible worldwide.

Running locally

If you want to test it locally, you may use ngrok.

In case if you have a problem with URL creation in your application, you can overwrite host header in your nginx host config file by adding the following parameters:

fastcgi_param HTTP_HOST your.ngrok.io;
fastcgi_param HTTPS on;

Replace your.ngrok.io with your actual ngrok URL

Tests

Run the following in the package folder:

vendor/bin/phpunit

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

laravel-saml2's People

Contributors

aacotroneo avatar abublihi avatar axis80 avatar breart avatar brendantwhite avatar danijelk avatar danmichaelo avatar darynmitchell avatar diegofonseca avatar dmyers avatar garethellis36 avatar graemetait avatar hughvolpe avatar jmandrade avatar joelpittet avatar joeyhoutenbos avatar jonathanwkelly avatar matijakovacevic avatar olivm avatar omitobi avatar owenvoke avatar perifer avatar quentinbontemps avatar robertboes avatar s3sam avatar snipe avatar soltmar avatar technowl avatar tupywebteam avatar vopolonc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-saml2's Issues

Multiple IdPs with different config?

My application uses 3 different IdPs. They all need a different configuration for example in the 'security' section.

To me it looks like you are using the same config/saml2.php for all tenants and only change 4 values (entityId, singleSignOnService, singleLogoutService, x509cert) Source.

Is this correct or did I miss something?

Do you have plans to change this in the future?
Are you interested in a PR for this feature?
How would you approach this?

Error calling Saml2Auth::logout()

I followed the readme and get this error in my Laravel app trying to logout. I placed Saml2Auth::logout() right after Auth::logout().

OneLogin\Saml2\Error
Settings file not found: /var/www/html/vendor/onelogin/php-saml/settings.php

Have you seen this before? I wasn't sure how to fix it.

Am I doing something wrong or should I be setting the tenant somehow? If so I'm not sure how to best do that if I need to be storing the tenant in a session myself or calling something in the package. It seems that the routes that are defined in the package have a route resolver that my logout route wouldn't have.

Signature validation failed. SAML Response rejected. (Azure SAML)

Hi everyone,

I've followed everything as explained, but in the end I get "Signature validation failed. SAML Response rejected" back.
The login URL does work, leading me eventually to the "/acs" route, where the error is then returned.

In the config file, I find these back

'x509cert' => env('SAML2_SP_CERT_x509',''),
'privateKey' => env('SAML2_SP_CERT_PRIVATEKEY',''),

Do they serve any purpose at all? I don't see them coming back in the README.md, but I don't see them being used anywhere in the code either.

I'm following along the example with an Azure AD SAML configuration and posting here because the issue seems to be coming back as a response from Azure itself, meaning I can't start debuggin my or the library's code.

Can not get current logged in user session to make logout & login the new user.

Hello,

The are some cases which don't work here:

  • If there is already logged user & it's email != saml->user->email, then make logged-in user logout first.
  • If there is already logged user & it's email == saml->user->email, then app don't need new session & need to persist existing session.

Problem is:

  • auth()->user() always returns null.
  • auth()->login($user) creates new session always. I guess its due to the fact the auth()->user() return null.

Correct me if I am wrong on concept & reported problems.

Redirect Loop

Hi,

I got a redirect loop on the Microsoft Login page. It keeps log in and log in and log in. The following code is my event listener.

namespace App\Listeners;

use Slides\Saml2\Events\SignedIn;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Auth;
use App\Providers\RouteServiceProvider;

class Saml2SignedIn
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  SignedIn  $event
     * @return void
     */
    public function handle(SignedIn $event)
    {
	    // your own code preventing reuse of a $messageId to stop replay attacks
	    $user = $event->getSaml2User();
	   
        $userEmail = $user->getAttributes()['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'][0];
        $name = $user->getAttributes()['http://schemas.microsoft.com/identity/claims/displayname'][0];
        
       $searchUser = \App\Models\User::where('email', $userEmail)->first();
       
       if ($searchUser) {

                Auth::login($searchUser, true);

                ray('login from existing user');
                ray($searchUser);
                
                return redirect('/');
            } else {

                $azureUser = \App\Models\User::create([
                    'name' => $name,
                    'email' => $userEmail,
                    'password' => bcrypt('password')
                ]);

                Auth::login($azureUser, true);
                
                ray('login from new user');

                return redirect(RouteServiceProvider::HOME);
            }
	    
    }
}

I'm able to see the login is success by adding ray debug ray('login from existing user'); . After logged in, it keeps redirect to the same login page again.

How to Change baseUrl config

Hello,

I keep getting this error:
{"error":"The response was received at http://localhost/saml2/***/acs instead of https://***.com/***/acs"}

I noticed this was due to the HTTP_HOST being localhost. I noticed there is config config named baseUrl in the onelogin settings_example.php file that can override the HTTP_HOST in the url generation. Since the packages are in the /vendor folder (which is in .gitignore) if I do some changes in the files they will not be applied in the repo.

Is there a setting in the published saml config file that can change the baseUrl? How can I achieve this?

Thanks!

Don't change 'NameIDFormat' in metadata url

Hello,

We have changed in our config file the value of 'NameIDFormat' but in the painted xml of the metadata another one continues to appear

Saml2.php

<?php

return [

    
    /*
    |--------------------------------------------------------------------------
    | Use built-in routes
    |--------------------------------------------------------------------------
    |
    | If "useRoutes" set to true, the package defines five new routes:
    |
    | Method | URI                               | Name
    | -------|-----------------------------------|------------------
    | POST   | {routesPrefix}/{idpKey}/acs       | saml.acs
    | GET    | {routesPrefix}/{idpKey}/login     | saml.login
    | GET    | {routesPrefix}/{idpKey}/logout    | saml.logout
    | GET    | {routesPrefix}/{idpKey}/metadata  | saml.metadata
    | GET    | {routesPrefix}/{idpKey}/sls       | saml.sls
    |
    */

    'useRoutes' => true,

    /*
    |--------------------------------------------------------------------------
    | Built-in routes prefix
    |--------------------------------------------------------------------------
    |
    | Here you may define the prefix for built-in routes.
    |
    */

    'routesPrefix' => '/auth',

    /*
    |--------------------------------------------------------------------------
    | Middle groups to use for the SAML routes
    |--------------------------------------------------------------------------
    |
    | Note, Laravel 5.2 requires a group which includes StartSession
    |
    */

    'routesMiddleware' => ['saml'],

    /*
    |--------------------------------------------------------------------------
    | Signature validation
    |--------------------------------------------------------------------------
    |
    | Set to true if you want to use parameters from $_SERVER to validate the signature.
    |
    */

    'retrieveParametersFromServer' => false,

    /*
    |--------------------------------------------------------------------------
    | Login redirection URL.
    |--------------------------------------------------------------------------
    |
    | The redirection URL after successful login.
    |
    */

    'loginRoute' => env('SAML2_LOGIN_URL'),

    /*
    |--------------------------------------------------------------------------
    | Logout redirection URL.
    |--------------------------------------------------------------------------
    |
    | The redirection URL after successful logout.
    |
    */

    'logoutRoute' => env('SAML2_LOGOUT_URL'),


    /*
    |--------------------------------------------------------------------------
    | Login error redirection URL.
    |--------------------------------------------------------------------------
    |
    | The redirection URL after login failing.
    |
    */

    'errorRoute' => env('SAML2_ERROR_URL'),

    /*
    |--------------------------------------------------------------------------
    | Strict mode.
    |--------------------------------------------------------------------------
    |
    | If 'strict' is True, then the PHP Toolkit will reject unsigned
    | or unencrypted messages if it expects them signed or encrypted
    | Also will reject the messages if not strictly follow the SAML
    | standard: Destination, NameId, Conditions... are validated too.
    |
    */

    'strict' => true,

    /*
    |--------------------------------------------------------------------------
    | Debug mode.
    |--------------------------------------------------------------------------
    |
    | When enabled, errors must be printed.
    |
    */

    'debug' => env('SAML2_DEBUG', env('APP_DEBUG', false)),

    /*
    |--------------------------------------------------------------------------
    | Whether to use `X-Forwarded-*` headers to determine port/domain/protocol.
    |--------------------------------------------------------------------------
    |
    | If 'proxyVars' is True, then the Saml lib will trust proxy headers
    | e.g X-Forwarded-Proto / HTTP_X_FORWARDED_PROTO. This is useful if
    | your application is running behind a load balancer which terminates SSL.
    |
    */

    'proxyVars' => false,

    /*
    |--------------------------------------------------------------------------
    | Service Provider configuration.
    |--------------------------------------------------------------------------
    |
    | General setting of the service provider.
    |
    */

    'sp' => [

        /*
        |--------------------------------------------------------------------------
        | NameID format.
        |--------------------------------------------------------------------------
        |
        | Specifies constraints on the name identifier to be used to
        | represent the requested subject.
        |
        */

        'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',

        /*
        |--------------------------------------------------------------------------
        | SP Certificates.
        |--------------------------------------------------------------------------
        |
        | Usually x509cert and privateKey of the SP are provided by files placed at
        | the certs folder. But we can also provide them with the following parameters.
        |
        */

        'x509cert' => env('SAML2_SP_CERT_x509',''),
        'privateKey' => env('SAML2_SP_CERT_PRIVATEKEY',''),

        /*
        |--------------------------------------------------------------------------
        | Identifier (URI) of the SP entity.
        |--------------------------------------------------------------------------
        |
        | Leave blank to use the 'saml.metadata' route.
        |
        */

        'entityId' => env('SAML2_SP_ENTITYID','contactcenter'),

        /*
        |--------------------------------------------------------------------------
        | The Assertion Consumer Service (ACS) URL.
        |--------------------------------------------------------------------------
        |
        | URL Location where the <Response> from the IdP will be returned, using HTTP-POST binding.
        | Leave blank to use the 'saml.acs' route.
        |
        */

        'assertionConsumerService' => [
            'url' => '',
        ],

        /*
        |--------------------------------------------------------------------------
        | The Single Logout Service URL.
        |--------------------------------------------------------------------------
        |
        | Specifies info about where and how the <Logout Response> message MUST be
        | returned to the requester, in this case our SP.
        |
        | URL Location where the <Response> from the IdP will be returned, using HTTP-Redirect binding.
        | Leave blank to use the 'saml.sls' route.
        |
        */

        'singleLogoutService' => [
            'url' => ''
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | OneLogin security settings.
    |--------------------------------------------------------------------------
    |
    |
    |
    */

    'security' => [

        /*
        |--------------------------------------------------------------------------
        | NameId encryption
        |--------------------------------------------------------------------------
        |
        | Indicates that the nameID of the <samlp:logoutRequest> sent by this SP
        | will be encrypted.
        |
        */

        'nameIdEncrypted' => false,

        /*
        |--------------------------------------------------------------------------
        | AuthnRequest signage
        |--------------------------------------------------------------------------
        |
        | Indicates whether the <samlp:AuthnRequest> messages sent by
        | this SP will be signed. The Metadata of the SP will offer this info
        |
        */

        'authnRequestsSigned' => false,

        /*
        |--------------------------------------------------------------------------
        | Logout request signage
        |--------------------------------------------------------------------------
        |
        | Indicates whether the <samlp:logoutRequest> messages sent by this SP
        | will be signed.
        |
        */

        'logoutRequestSigned' => false,

        /*
        |--------------------------------------------------------------------------
        | Logout response signage
        |--------------------------------------------------------------------------
        |
        | Indicates whether the <samlp:logoutResponse> messages sent by this SP
        | will be signed.
        |
        */

        'logoutResponseSigned' => false,

        /*
        |--------------------------------------------------------------------------
        | Whether need to sign metadata.
        |--------------------------------------------------------------------------
        |
        | The possible values:
        | - false
        | - true (use certs)
        | - array:
        |   ```
        |   [
        |       'keyFileName' => 'metadata.key',
        |       'certFileName' => 'metadata.crt'
        |   ]
        |   ```
        |
        */

        'signMetadata' => false,

        /*
        |--------------------------------------------------------------------------
        | Requirement to sign messages.
        |--------------------------------------------------------------------------
        |
        | Indicates a requirement for the <samlp:Response>, <samlp:LogoutRequest> and
        | <samlp:LogoutResponse> elements received by this SP to be signed.
        |
        */

        'wantMessagesSigned' => false,

        /*
        |--------------------------------------------------------------------------
        | Requirement to sign assertion elements.
        |--------------------------------------------------------------------------
        |
        | Indicates a requirement for the <saml:Assertion> elements received by
        | this SP to be signed.
        |
        */

        'wantAssertionsSigned' => false,

        /*
        |--------------------------------------------------------------------------
        | Requirement to encrypt NameID.
        |--------------------------------------------------------------------------
        |
        | Indicates a requirement for the NameID received by this SP to be encrypted.
        |
        */

        'wantNameIdEncrypted' => false,

        /*
        |--------------------------------------------------------------------------
        | Authentication context.
        |--------------------------------------------------------------------------
        |
        | Set to false and no AuthContext will be sent in the AuthNRequest,
        |
        | Set true or don't present this parameter and you will get an
        | AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
        |
        | Set an array with the possible auth context values:
        | ['urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509']
        |
        */

        'requestedAuthnContext' => true,
    ],

    /*
    |--------------------------------------------------------------------------
    | Contact information.
    |--------------------------------------------------------------------------
    |
    | It is recommended to supply a technical and support contacts.
    |
    */

    'contactPerson' => [
        'technical' => [
            'givenName' => env('SAML2_CONTACT_TECHNICAL_NAME', 'name'),
            'emailAddress' => env('SAML2_CONTACT_TECHNICAL_EMAIL', '[email protected]')
        ],
        'support' => [
            'givenName' => env('SAML2_CONTACT_SUPPORT_NAME', 'Support'),
            'emailAddress' => env('SAML2_CONTACT_SUPPORT_EMAIL', '[email protected]')
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Organization information.
    |--------------------------------------------------------------------------
    |
    | The info in en_US lang is recommended, add more if required.
    |
    */

    'organization' => [
        'en-US' => [
            'name' => env('SAML2_ORGANIZATION_NAME', 'Name'),
            'displayname' => env('SAML2_ORGANIZATION_NAME', 'Display Name'),
            'url' => env('SAML2_ORGANIZATION_URL', 'http://url')
        ],
    ],

];

http://dev.contactcenter.foticos.com/auth/UUID/metadata

This XML file does not appear to have any style information associated with it. The document tree is shown below.
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2022-06-11T13:22:50Z" cacheDuration="PT604800S" entityID="contactcenter">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://domain/auth/uuid/sls"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://domain/auth/uuid/acs" index="1"/>
</md:SPSSODescriptor>
<md:Organization>
<md:OrganizationName xml:lang="en-US">Name</md:OrganizationName>
<md:OrganizationDisplayName xml:lang="en-US">Display Name</md:OrganizationDisplayName>
<md:OrganizationURL xml:lang="en-US">http://url</md:OrganizationURL>
</md:Organization>
<md:ContactPerson contactType="technical">
<md:GivenName>name</md:GivenName>
<md:EmailAddress>[email protected]</md:EmailAddress>
</md:ContactPerson>
<md:ContactPerson contactType="support">
<md:GivenName>Support</md:GivenName>
<md:EmailAddress>[email protected]</md:EmailAddress>
</md:ContactPerson>
</md:EntityDescriptor>

Bug with saml_url ?

Hello,

I've tried these different formats:

$url = saml_url(config('app.url'), $identity->uuid, [config('app.url') . '/login-sso-redirect']);
$url = saml_url(config('app.url'), $identity->uuid, config('app.url') . '/login-sso-redirect');
$url = saml_url(config('app.url'), $identity->uuid, ['returnTo' => config('app.url') . '/login-sso-redirect']);

But the result is still the same, and without my return parameter.

https://xxx/saml2/2365c568-1d1d-410e-88cd-6b85b643da33/login?returnTo=xxx

Question: Where to add listeners for events?

Hi,

the readme says under "Authentication events", that you should "add listeners on Slides\Saml2\SignedIn and Slides\Saml2\SignedOut events.", but it does not tell where (in which file) one should do that - is there any example showing exactly what to do?

kind regards,

Christoph

Target class [saml] does not exist

Hi
Can you please help me fix the above error?

I have followed the documentation and created the tenant and added the middleware checking.

When I try to access the urls I am getting the attached error.

Please advice.

Thanks

Pearl
snapshot-1675337441376

Error in script for deleting tenants

Hi, there seems to be a bug in the console script for manipulating/deleting tenants.
Specifing either an ID or a UUID leads to a SQL query syntax error.
The value cannot be an integer and a string at the same time.

The backend database is PostgreSQL. Maybe other databases are more lenient with this
but Postgres unfortunately not.

j@dev:~/vgp [saml2*]$ ./artisan saml2:delete-tenant 1a86e4e9-d8d0-4411-ac66-f4743f0250c6

   Illuminate\Database\QueryException 

  SQLSTATE[22P02]: Invalid text representation: 7 ERROR:  invalid input syntax for integer: "1a86e4e9-d8d0-4411-ac66-f4743f0250c6" (SQL: select * from "saml2_tenants" where ("id" = 1a86e4e9-d8d0-4411-ac66-f4743f0250c6 or "key" = 1a86e4e9-d8d0-4411-ac66-f4743f0250c6 or "uuid" = 1a86e4e9-d8d0-4411-ac66-f4743f0250c6) and "saml2_tenants"."deleted_at" is null)

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:692
    688▕         // If an exception occurs when attempting to run a query, we'll format the error
    689▕         // message to include the bindings with SQL, which will make this exception a
    690▕         // lot more helpful to the developer instead of just the database's errors.
    691▕         catch (Exception $e) {
  ➜ 692▕             throw new QueryException(
    693▕                 $query, $this->prepareBindings($bindings), $e
    694▕             );
    695▕         }
    696▕     }

      +29 vendor frames 
  30  artisan:37
      Illuminate\Foundation\Console\Kernel::handle()
j@dev:~/vgp [saml2*]$ 

The problem seems to lie in this method in class Slides\Saml2\Repositories\TenantRepository:

    /**
     * Find a tenant by any identifier.
     *
     * @param int|string $key ID, key or UUID
     * @param bool $withTrashed Whether need to include safely deleted records.
     *
     * @return Tenant[]|\Illuminate\Database\Eloquent\Collection
     */
    public function findByAnyIdentifier($key, bool $withTrashed = true)
    {   
        return $this->query($withTrashed)
            ->where('id', $key)
            ->orWhere('key', $key)
            ->orWhere('uuid', $key)
            ->get();
    }

Looks like a check is missing if $key is an integer (for 'id') or a text value (for 'uuid', or 'key').

Thanks.

x509 Certificate

If I have a certificate file (.crt) with a private key (.key). what is the proper openssl command to generate the x509 certificate needed for this package?

How to handle RequestDenied responses correctly?

Currently if I'm trying to sign in a user that has no permission to the application signing in to, if got an error 500:

[2022-11-29 13:57:14] staging.ERROR: saml2.error_detail {"error":"The status code of the Response was not Success, was Responder -&gt; urn:oasis:names:tc:SAML:2.0:status:RequestDenied"}
[2022-11-29 13:57:14] staging.ERROR: saml2.error ["invalid_response"]
[2022-11-29 13:57:14] staging.ERROR: Symfony\Component\HttpFoundation\Response::setContent(): Argument #1 ($content) must be of type ?string, Illuminate\Routing\Redirector given, called in .../vendor/laravel/framework/src/Illuminate/Http/Response.php on line 72 {"exception":"[object] (TypeError(code: 0): Symfony\\Component\\HttpFoundation\\Response::setContent(): Argument #1 ($content) must be of type ?string, Illuminate\\Routing\\Redirector given, called in .../vendor/laravel/framework/src/Illuminate/Http/Response.php on line 72 at .../vendor/symfony/http-foundation/Response.php:412)

I am handling the login of the user in an Event listener. Where can I catch the exception and handle it correctly?

Redirect Loop after SSO

I configured my IDP and followed the instructions given in the Readme section.

When I call https://url-of-my-website/saml2/{uuid}/login I see the SSO page of my IDP.
After successfully logging in I am ending up in a redirect loop:
https://my-idp-sso-provider/saml2/idp/?SAMLRequest=&RelayState=https://url-of-my-website/saml2/{uuid}/login

For each redirect I see [2022-08-09 18:06:28] local.DEBUG: [Saml2] Tenant resolved {"uuid":"uuid","id":1,"key":"sso_key"} in my laravel log.

`
Event::listen(SignedIn::class, function (SignedIn $event) {

        $messageId = $event->getAuth()->getLastMessageId();

        // your own code preventing reuse of a $messageId to stop replay attacks
        $samlUser = $event->getSaml2User();

        $userData = [
            'id' => $samlUser->getUserId(),
            'attributes' => $samlUser->getAttributes(),
            'assertion' => $samlUser->getRawSamlAssertion()
        ];

        // Just dump login works
        //dd($userData);

        $username = $userData["attributes"]["username"]; // find user by ID or attribute
        $user = User::where('name',$username) -> first();

        // Login a user.
        Auth::loginUsingId($user->id);
    });

`

From the Kernel
`
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],

    'saml' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
    ],

    'api' => [
        // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

`

From saml2.php
'routesMiddleware' => ['saml'],

Need Documentation for IDP

I hope this package was handle IDP also, if so can we get a documentation for how to configure that, or just add a link in for IDP, AFIK in the readme everything is about SP only, or am I the one understand it wrong

Adding Certificates per tenant

Hi,

I appreciate your work on this package, it is helping me a lot during development.

Currently, I am having some issues regarding adding the certificates in production environment. In development, the only working method to me is by putting the sp.crt and sp.key inside vendor/onelogin/php-saml/certs folder and it is working fine. but as per Onelogin docs:

Important In this option, the x509 certs must be stored at vendor/onelogin/php-saml/certs and settings file stored at vendor/onelogin/php-saml.

Your settings are at risk of being deleted when updating packages using composer update or similar commands. So it is highly recommended that instead of using settings files, you pass the settings as an array directly to the constructor (explained later in this document). If you do not use this approach your settings are at risk of being deleted when updating packages using composer update or similar commands.

so I can't figure out how to add sp.crt and sp.key per tenant in production.

As per your docs:

x509 certificate — The certificate provided by Identity Provider in base64 format

if this is the sp.crt, what about the sp.key? where to add it?

kindly I need your support

Questions about certificate expiration

Hello, thank you for this package.

I have some questions about certificate expiration management.

  • If the IdP's certificate has expired, will it still work? (apparently the onelogin package does not handle this case)

  • If the IdP's certificate has changed, wouldn't it be better to to automatically update it in the database from the metadata?

  • what are the best practices if we change the sp certificate ? is there a way to automatically warn the idp other than the updated metadata file ?

Thanks a lot !

Q: Is config.php value NameIDFormat no longer used?

Q: Should NameIDFormat be removed from the config file?

Details:

Commit 7cdac60 introduced per-tenant nameID format, stored in DB and defaulting to persistent

The config file still has a value NameIDFormat

    'sp' => [

        /*
        |--------------------------------------------------------------------------
        | NameID format.
        |--------------------------------------------------------------------------
        |
        | Specifies constraints on the name identifier to be used to
        | represent the requested subject.
        |
        */

        'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',

But I think that value is now ignored.
Is that correct?

If so I can open a PR to remove that from config.php

Vendor publish error

Hi, i am facing an issue when i am trying to publish vendor. The error says:
Can't locate path: </var/www/html/vendor/24slides/laravel-saml2/src../config/saml2.php>
I am using laravel 5.5 and php 7.0. The 24slides/laravel-saml2 version that composer installed is 2.0.0.
Any ideas what is the error? I believe that the error is the src.. in the path but i can not figure out how can i fix this.
Thanks in advance!

Support for Laravel 7

Hi @brezzhnev ,

Do you have instructions to upgrade to Laravel 7? I am trying to upgrade my app to Laravel 7 and this package blocks the update.

You helped me with upgrading to L6 while ago and I am hoping to do the same with L7.

Thanks for any guidance.

Change environment variable in config for SAML2_SP_CERT_x509 to all uppercase

As the title suggests, I think it might be a good idea to change the expected env var for attaching the sp certifitcate to be SAML2_SP_CERT_X509 (The X uppercase), since the convention tends to be that these variables will be all uppercase, however are treated case sensitively.

The reason I came across this is because I am using this package with Laravel Vapor, and the interface for adding 'secrets' (which are what it uses for environment variables) only allows inputing key names in uppercase which meant I need to modify the variable in the config to account for this.

While obviously most people wouldn't come across this limitation, it also seems like using all uppercase for the default config would be a reasonable convention to follow, and would avoid this and similar issues for new users of this package.

Thoughts on Multiple IDP certificates support?

I believe this is the x509certMulti settings in OneLogin.

From their docs:

IdP with multiple certificates
In some scenarios the IdP uses different certificates for signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata.

Session is clearing when hitting the ACS route

I have an application using this plugin. Before the user logs in, the app saves a piece of data in a session. I think have them follow a link generated by the saml_route() helper function. The user is redirected to the SSO, they signin, and are directed back to the ACS route. By the time the code get to the acs() method in the src/Http/Controllers/Saml2Controller.php controller, the existing session is cleared and a new one has started.

I traced back through the middleware and the previous session data exists as the request passes through the ResolveTenant middleware and the ones described in the readme.md file:

\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,

I have followed through the stacktrace but cannot figure out what is deleting my existing session and starting a new one.

Any ideas what is causing this?

Maintained

Is this package still being maintained? I saw it was a fork of another package and was just curious since I've started integrating with it.

Option to prevent migrations running automatically.

I'm trying to use this package alongside the multi-tenancy package https://github.com/archtechx/tenancy.

This package creates seperate databases for the tenants while using the default database as a 'central' database.

The SAML2 tables need to be added to the individual tenant DBs not this 'central' database, but because this package uses "loadMigrationsFrom" I do not have control over how these migrations are loaded and run.

The Multi tenancy package allows me to specify additional 'paths' to use for tenant migrations, so I can include the path to the laravel-saml2 migrations directly to get them to run where I need them. However they also still run on the 'central' DB.

What I am hoping is that an option can be added to this package to be able to disable the "loadMigrationsFrom" command, so that they do not need to be run automatically in situations like mine where they are being run by a different method.

Do you think this is something that could be included in this package (assuming a solution to this does not already exist)?

If so, I can make a PR for this.

logout redirect issue

For the first case, call Saml2Auth::logout(); or redirect the user to the route saml.logout which does just that. Do not close the session immediately as you need to receive a response confirmation from the IdP (redirection). That response will be handled by the library at /saml2/sls and will fire an event for you to complete the operation.

First of all, I have config'd my app based on the instructions found in the README file. Second, I am able to login just fine.

Now, I am having issues w/ the logout process, once I hit the /saml/{UUID}/logout route WITH or WITHOUT the returnTo parameter set, I always end up stuck in MS Azure's logout screen. No redirect is happening at all.

If I do the same, but for the /saml/{UUID}/login route, everything is a-okay. Why is MS Azure not redirecting back to my app after a successful logout? Does the sls route need to be configured in MS Azure?

What am I missing? Please advise.

Package routes are not getting registered (automatically)

I am now replicating the package in production server, so config is all that's left. Followed everything based on initial setup and README, but I am still getting this error when creating the tenant:

image

DB table is populated, yes, but the error is there. And I keep on getting a 404 page coz the SAML2 routes are not a part of my current artisan route:list. I would like to use the default route that are supposed to be registered automatically. What am I missing here?

ServiceProvider boot calls migrations

Slides\Saml2\ServiceProvider.boot() calls $this->loadMigrations();
Why would it be necessary to call this every time the ServiceProvider is instantiated
Is this is not a one-time set-up step?

Support for Laravel 6

Do you have instructions to upgrade to Laravel 6? I am trying to upgrade my app to Laravel 6 and this package blocks the update.

Thanks for any guidance.

Authentication Persistence

Hello, I have issue regarding the authentication, I logged in using the Event listener "UserLoggedIn". But once I get the Auth::user() in Controller, I'm getting a null value.

I also tried saving the userData in Session but once I try to obtain the value of the Session, I'm getting null. Maybe middleware issue?

I'm using Laravel 8+ and NextJS

UserLoggedIn

<?php

namespace App\Listeners;

use App\Models\User;
use Illuminate\Http\Request;
use Slides\Saml2\Events\SignedIn;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;

class UserLoggedIn
{
    public $request;

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct(Request $request)
    {
        //
        $this->request = $request;
    }

    /**
     * Handle the event.
     *
     * @param  Slides\Saml2\Events\SignedIn  $event
     * @return void
     */
    public function handle(SignedIn $event)
{
        $messageId = $event->auth->getLastMessageId();
        // your own code preventing reuse of a $messageId to stop replay attacks
        $samlUser = $event->auth->getSaml2User();
        $userData = [
            'id' => $samlUser->getUserId(),
            'attributes' => $samlUser->getAttributes(),
            'assertion' => $samlUser->getRawSamlAssertion()
        ];

        // dd($userData);
        // Login a user.
        // Auth::login($user);
        $searchUser = User::where('email', $userData['attributes']['email'])->first();

        if ($searchUser) {
            Auth::login($searchUser);
        } else {
            $newUser = User::create([
                'name' => $userData['attributes']['first_name'][0],
                'email' => $userData['attributes']['email'][0],
                'role' => 'admin',
                'password' => bcrypt('password')
            ]);

            Auth::login($newUser);
        }
    }
}

Kernel.php

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array<int, class-string|string>
     */
    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        // \Illuminate\Session\Middleware\StartSession::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array<string, array<int, class-string|string>>
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        // \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'saml' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            // \App\Http\Middleware\SAMLAuthenticated::class,
            // \App\Listeners\UserLoggedIn::class,
            // \Slides\Saml2\Events\SignedIn::class,
            // \App\Listeners\UserLoggedIn::class,
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array<string, class-string|string>
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \App\Http\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'auth.saml' => \App\Http\Middleware\SAMLAuthenticated::class,
    ];
}

api.php

<?php

use App\Http\Controllers\AuthController;
use App\Http\Controllers\ContactInfoController;
use App\Http\Controllers\InitiativeController;
use App\Http\Controllers\PageController;
use App\Http\Controllers\PillarController;
use App\Http\Controllers\ProgramController;
use App\Http\Controllers\SDGContributionController;
use App\Http\Controllers\SDGProgressController;
use App\Http\Controllers\SDGStrategyController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

    // Route::middleware('saml')->group(function () {
        Route::post('create-page', [PageController::class, 'store']);
        Route::get('all-pages', [PageController::class, 'showAll']);
        Route::get('single-page/{id}', [PageController::class, 'show']);
        Route::post('edit-page/{id}', [PageController::class, 'update']);
        Route::delete('remove-page/{id}', [PageController::class, 'destroy']);
        Route::post('publish-page/{id}', [PageController::class, 'publish']);

        Route::post('create-sdg-strategy', [SDGStrategyController::class, 'store']);
        Route::get('all-sdg-strategies', [SDGStrategyController::class, 'showAll']);
        Route::get('single-sdg-strategy/{id}', [SDGStrategyController::class, 'show']);
        Route::post('edit-sdg-strategy/{id}', [SDGStrategyController::class, 'update']);
        Route::delete('remove-sdg-strategy/{id}', [SDGStrategyController::class, 'destroy']);

        Route::post('create-sdg-progress', [SDGProgressController::class, 'store']);
        Route::get('all-sdg-progresses', [SDGProgressController::class, 'showAll']);
        Route::get('single-sdg-progress/{id}', [SDGProgressController::class, 'show']);
        Route::post('edit-sdg-progress/{id}', [SDGProgressController::class, 'update']);
        Route::delete('remove-sdg-progress/{id}', [SDGProgressController::class, 'destroy']);

        Route::post('create-pillar', [PillarController::class, 'store']);
        Route::get('all-pillars', [PillarController::class, 'showAll']);
        Route::get('single-pillar/{id}', [PillarController::class, 'show']);
        Route::post('edit-pillar/{id}', [PillarController::class, 'update']);
        Route::delete('remove-pillar/{id}', [PillarController::class, 'destroy']);

        Route::post('create-initiative', [InitiativeController::class, 'store']);
        Route::get('all-initiatives', [InitiativeController::class, 'showAll']);
        Route::get('single-initiative/{id}', [InitiativeController::class, 'show']);
        Route::post('edit-initiative/{id}', [InitiativeController::class, 'update']);
        Route::delete('remove-initiative/{id}', [InitiativeController::class, 'destroy']);

        Route::get('all-contact-infos', [ContactInfoController::class, 'showAll']);
        Route::get('single-contact-info/{id}', [ContactInfoController::class, 'show']);
        Route::post('edit-contact-info/{id}', [ContactInfoController::class, 'update']);

        Route::post('create-program', [ProgramController::class, 'store']);
        Route::get('all-programs', [ProgramController::class, 'showAll']);
        Route::get('single-program/{id}', [ProgramController::class, 'show']);
        Route::post('edit-program/{id}', [ProgramController::class, 'update']);
        Route::delete('remove-program/{id}', [ProgramController::class, 'destroy']);

        // Secured routes go here
        Route::get('test', function(Request $request) {
            Session::put('userData2', 'sample_user26');
            Session::save();
        });

        Route::get('login', function () {
            return response()->json([
                'URL' => 'http://localhost:8000/saml2/51af91b9-b072-4654-9be8-9f947989a879/login',
            ], 200);
        });
        Route::redirect('logout', '/saml2/51af91b9-b072-4654-9be8-9f947989a879/logout')->name('logout');
    // });

    Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
        return $request->user();
    });

auth.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option controls the default authentication "guard" and password
    | reset options for your application. You may change these defaults
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "session"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'saml' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | You may specify multiple password reset configurations if you have more
    | than one user table or model in the application and you want to have
    | separate password reset settings based on the specific user types.
    |
    | The expire time is the number of minutes that each reset token will be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Password Confirmation Timeout
    |--------------------------------------------------------------------------
    |
    | Here you may define the amount of seconds before a password confirmation
    | times out and the user is prompted to re-enter their password via the
    | confirmation screen. By default, the timeout lasts for three hours.
    |
    */

    'password_timeout' => 10800,

];

EventServiceProvider

<?php

namespace App\Providers;

use App\Listeners\UserLoggedIn;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Session;
use Slides\Saml2\Events\SignedIn;
use Slides\Saml2\Events\SignedOut;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event to listener mappings for the application.
     *
     * @var array<class-string, array<int, class-string>>
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        SignedIn::class => [
            UserLoggedIn::class,
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        Event::listen(Slides\Saml2\Events\SignedOut::class, function (SignedOut $event) {
            Auth::logout();
            Session::save();
        });
    }

    /**
     * Determine if events and listeners should be automatically discovered.
     *
     * @return bool
     */
    public function shouldDiscoverEvents()
    {
        return true;
    }
}

.env

SANCTUM_STATEFUL_DOMAINS=web.globe-cms.test
SESSION_DOMAIN=.globe-cms.test

Handling SP Initiated Login

First of all, thanks for the great package. SSO is such a confusing thing, but this package makes it much simpler.

I've been able to get IPD initiated logins working very quickly.

I'd like to ask what is the best way to add SP initiated login to the system. How do you figure out which user is linked to which tenant?

Thank you in advance

Call to undefined method

Hi. When i am trying to get messageId i have the following error
Call to undefined method Slides\Saml2\Events\SignedIn::getAuth()

Any idea?

Custom routes arent fully working

This is more of a discussion than a bug report.

I am using sub-domains to identify my tenants, not a uuid in the path.

So, one tenant might access my app via:
tenant1.myapplication.com

To achieve this, I have set saml2.useRoutes to false, and removed the default UUID from the route.

Then, I had to replace the ResolveTenant middleware, to find a tenant first from my own tenants table, then get the saml2_tenants via a relationship.

I also had to extend part of OneLoginBuilder, the configDefaultValues method, to use the correct routes, instead of this URL::route('saml.metadata', ['uuid' => $this->tenant->uuid]).

This is needed because the current method would generate:
tenant1.myapplication.com/saml2/metadata?uuid=123e4567-e89b-12d3-a456-426614174000

The IdP doesnt like. It would be better as:
tenant1.myapplication.com/saml2/metadata

This isnt much work, but feels like I am going against the package.

Would you be open to a discussion/ a PR around having a new way to resolve tenants based on a saml_tenants.sub_domain value.

Or, ideally, would be to use it from a custom tenant model (#49)!

Right now, its quite an opinionated package, but we could make it GREAT! Im excited to hear your reply!

Support for Laravel 8

Hi @brezzhnev

Do you have instructions to upgrade to Laravel 8? I am trying to upgrade my app to Laravel 8 and this package blocks the update.

You helped me with upgrading to L7 a while ago and I am hoping to do the same with L8.
#4

Thanks for any guidance.

Error message shown when IDP response validation is invalid is a Symfony TypeError instead of the real error

Below is the error that is returned when the response from the IDP is determined to be invalid.

Symfony\Component\HttpFoundation\Response::setContent(): Argument #1 ($content) must be of type ?string, Illuminate\Routing\Redirector given, called in \vendor\laravel\framework\src\Illuminate\Http\Response.php on line 72

I came across this when I missed copying part of the IDP cert into idp_x509_cert.

I'm not sure this can be fixed in this package as it seems to be a Laravel issue, but I thought I'd put it here in case someone could figure out something. Worst case scenario, someone else who can't properly copy/paste (like me today) can fix the issue faster.

Currently using Laravel 10.7.1 and laravel-saml2 2.2.0

fix Step 2. Publish the configuration file.

Its not correct:
php artisan vendor:publish --provider="Slides\Saml2\ServiceProvider

the correct is without (")
php artisan vendor:publish --provider=Slides\Saml2\ServiceProvider

Dynamic IDP SingleSignOnUrl

Hi @brezzhnev / @dmyers !

Currently i'm migrating from the old(er) package "aacotroneo/laravel-saml2" to yours.
One thing that I about this version is that i'm in control of the SingleSignOnService URL.

"...../saml2/idp/SSOService.php"

In the older package this was written in the config.
Anything related to the IDP isn't configurable anymore.

I would like to make this dynamic, with an extra PHP function.

For example:

"...../saml2/idp/SSOService.php".myFunction();

/**
 * @return string
 */
function myFunction()
{
if (isset($_GET['admin']){
$source = 'admin';
}
$source ='user';

return '?source=' . $source;

}

Previous you could do this in the config, but it looks like the "OneLoginBuilder" is already setting it (throughout my db)

$oneLoginConfig['idp'] = [
                'entityId' => $this->tenant->idp_entity_id,
                'singleSignOnService' => ['url' => $this->tenant->idp_login_url],
                'singleLogoutService' => ['url' => $this->tenant->idp_logout_url],
                'x509cert' => $this->tenant->idp_x509_cert
            ];

I'm looking forward to your solution, where can I overrule this "singleSignOnService url"?

Possible to use idpName instead of uuid?

I am attempting to migrate from aacotroneo/laravel-saml2 to this package. In the old package, I used the idpName in the URL. When I create a tenant in this new package, it seems to want me to use a UUID in the URLs. For full context, here is my route:list output:

POST      saml2/{idpName}/acs ................................ saml2_acs › Aacotroneo\Saml2 › Saml2Controller@acs
GET|HEAD  saml2/{idpName}/login .......................... saml2_login › Aacotroneo\Saml2 › Saml2Controller@login
GET|HEAD  saml2/{idpName}/logout ....................... saml2_logout › Aacotroneo\Saml2 › Saml2Controller@logout
GET|HEAD  saml2/{idpName}/metadata ................. saml2_metadata › Aacotroneo\Saml2 › Saml2Controller@metadata
GET|HEAD  saml2/{idpName}/sls ................................ saml2_sls › Aacotroneo\Saml2 › Saml2Controller@sls
POST      saml2/{uuid}/acs ........................................ saml.acs › Slides\Saml2 › Saml2Controller@acs
GET|HEAD  saml2/{uuid}/login .................................. saml.login › Slides\Saml2 › Saml2Controller@login
GET|HEAD  saml2/{uuid}/logout ............................... saml.logout › Slides\Saml2 › Saml2Controller@logout
GET|HEAD  saml2/{uuid}/metadata ......................... saml.metadata › Slides\Saml2 › Saml2Controller@metadata
GET|HEAD  saml2/{uuid}/sls ........................................ saml.sls › Slides\Saml2 › Saml2Controller@sls

We use on-prem ADFS for our SSO, and I have all of the IDP/Relying Party stuff setup, and I just want to migrate over to this new (actively-supported) package. Practically speaking, I will only ever have 1 tenant. How can I keep the same URL scheme as before, when I was using the Aacotroneo package?

Thanks in advance!

Question - Protecting routes

Hi all.

Rather noob question, but I'm struggling to figure out how to protect some routes in by /routes/web.php file behind SAML auth.

I added the Middleware items noted in the readme.md at https://github.com/24Slides/laravel-saml2#middleware and added "->middleware('saml');" to the routes I want to ensure are behind SAML auth. When I go to those routes though in a new/incognito browser session, the view loads, where I was hoping it would redirect first through SAML.

I did add the Auth events listener from https://github.com/24Slides/laravel-saml2#authentication-events - slightly modified to get some extra attributes from the IdP - and I have the auth working OK and even have on-demand/JIT user provisioning to the Users model on appropriate accounts. This is using the redirect link method - e.g. https://my.website/saml2/UUID/login?returnTo=https://my.website/page/123 - so I am sure SAML auth itself is working OK.

Is there something simple that I'm missing here? I only have one IdP configured, so I was hoping to have a simple "->middleware('saml')" solution for my routes that would redirect any unauth'd requests to the IdP first.

Cheers!

Redirect users to different routes.

After user logged in, I would like to redirect to a specific page. Each user may be redirected to a different page, depending on some settings. There is a method called getIntendedUrl, which I believe is the most correct to use. however I can not identify a function where I can set this information. Does anyone know if there is a way, without having to implement the 'acs' method?

One Login setup

Hello.

Has anyone got this working with OneLogin? Our implementation works great with Azure AD but I've tried a variety of configurations in OneLogin and nothing seems to work. In our SamlSignInListener I've added

if (isset($event->user)) {
    Log::channel('stdout')->info(json_encode($event->user));
    Log::channel('stdout')->info(json_encode($event->auth));
}

Both always return null.

User attribute mapping

I've been working to integrate this package and was thinking it would be nice if it supported like a config of mapping of urn:oid values to model attributes automatically which then can be used on the model such as this:

$attributes = $samlUser->mappedAttributes();
$laravelUser->fill($attributes);

The config could look something like this:

return [
        'first_name' => [
            'urn:oid:2.5.4.42',
            'firstName',
        ],
];

And the Saml2User method:

    protected function mappedAttributes(): array
    {
       $attributes = $this->getAttributes();
        $result = collect(config('saml2.attribute_mapping')
            ->mapWithKeys(function ($map, $attribute) use ($attributes) {
                foreach ($map as $key) {
                    $values = Arr::get($attributes, $key);
                    if (empty($values) || !is_array($values)) continue;
                    $value = Arr::first($values);
                    if (empty($value)) continue;
                    return [$attribute => $value];
                }
                return [$attribute => null];
            });
        }

        return $result->all();
    }

Routes for Single Log Out

Firstly, according to the documentation, "By logging out of the global SSO Session. In this case the IdP will notify you on /saml2/{uuid}/slo endpoint (already provided)." however an /slo endpoint does not appear to be included in the routes specified in the packages routes file.

I notice that an /sls endpoint is also mentioned (which does exist). Are these 2 seperate endpoints or is this just a typo.

Secondly, assuming the /sls endpoint is correct, the behaviour of the /sls endpoint does not appear to match the actions performed upon logout by my idp (OKTA), which is making a POST request to that endpoint (as outlined here). The /sls endpont only accepts GET requests and additionally does not appear to pass on any data related to the logout when it fires the SignedOut event, which seems to contain no methods. This data would be useful to ensuring that I am logging out the correct user from an IDP initiated logout.

It looks like i'm going to need to implement my own endpoint to handle SLO, but just wanted to check if there was something I was missing, since this is described as being implemented according to the docs.

Thanks in advance for any light you can shed on this.

Stop replay attacks

In the documentation there is a comment about replay attacks:

$messageId = $event->getAuth()->getLastMessageId();
// your own code preventing reuse of a $messageId to stop replay attacks

What do we need to do here?

I found this answer an Stackoverflow, about InResponseTo. Is there a way to get this InResponseTo? Doesn't this require another event before the login?

Infinite loop login

Hi.

I am having a problem, when my login event is fired it creates an infinite loop as if it does not save the session or does not register the user.

` // SAML Login
Event::listen(SignedIn::class, function (SignedIn $event) {
Log::info('SignedIn');

        $saml2user = $event->getSaml2User();
    
        if ($saml2user) {
            $username = $saml2user->getUserId();
            $user = User::where('username', $username)->first();
            if ($user) {

                // \Session::push('name', 'test');
                Auth::login($user);
                Session::save();
            } 
    });`

This is what my session saves.

a:6:{s:5:"saml2"; a:1:{s:6:"tenant";a:0:{}} s:6:"_flash"; a:2:{s:3:"new";a:0:{}s:3:"old";a:0:{}} s:6:"_token"; s:40:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx s:50:"login_web_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";i:45;s:9:"_previous"; a:1:{s:3:"url";s:73:"https://group.net/images/logo/favicon.png";} s:17:"password_hash_web";s:60:"$xxxx$xxxx$xxxxxxxxxxxxxxxxxxx.xxxxxxx.xxxxx.xxxxxxxxxxxx";}

How to check "isAuthenticated()"

Not so much an issue but more of a question. I'm trying to use the Auth->isAuthenticated() function but can't for the life of me get the Auth model instantiated without jumping through a bunch of hoops trying to reverse engineer the middleware that sets the settings and everything. Has anyone done this before on this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.