What is Single Sign On?

Single Sign On – is a technology that lets the user authenticated on Identity Provider (further IdP), be automatically authenticated on another service (further Service Provider, SP or Consumer[1-N]) of the company.

Single Sign On is used by such Internet services as GoogleYandex and some others. Advantages of this approach are evident:

  • User enters pasword only once during the session.
  • Or doesn’t enter the password on IdP at all, if he is logged in via social service or OpenID.
  • User is automatically authenticated on all company’s projects.
  • User’s data can flow transparently from IdP to SP.

The cons derive from the pros:

  • Loss of IdP password implies the problem of signing in on all other services.
  • Potentially increased risk of theft of the master’s session from IdP (can be reduced by linking the session to ISP-subnet, and also by using HTTPS, HTTP Only Cookies and SSL Only Cookies)
  • Potentially increased risk of theft of the IdP password.

From business point of view and UX the pros outweigh the cons and companies usually decide to implement Single Sign On.

Before proceeding to the implementation of the SSO it would be good to ensure you know well the following:

and how to exploit it.

This is important because a wrong implementation of SSO, may lead to critical errors in all services that are connected to this SSO. For example user data compromised in one service or stolen IdP account entail data compromise in all services.

After getting familiar with basics: what is SSO and its security aspects you can proceed to implementing Single Sign On.

How does it work?

Generally authentication will work the following way:

Lets consider the case when user visits any page protected by authorization (point 1 on the scheme). Then Symfony2 activates an Entry point and redirects user to your IdP, where user gets OTP during his redirection trip. There may be several ways to continue depending on the following events:

  1. User is authenticated on IdP so IdP adds OTP to URL during redirection (point 3 on the scheme, green line)
  2. User isn’t authenticated on IdP so he should be redirected to a login form (point 3 on the scheme, red line)
  3. User visits us for the first time, he wants to sing-up and goes to sign-up form (at this time his last SP-name is already saved in IdP session)

After the registration he should be redirected (point 3, green line) to his last SP for OTP validation. When OTP is being validated, SP makes trusted REST request to IdP to ensure that OTP really exists and didn’t expire. At this point REST API should invalidate this OTP. You have to acquire a lock because this operation should be atomic. Further requests with this OTP return either HTTP 400 or HTTP 404 for SP.

In case IdP confirms that this OTP exists and is valid, SP authenticates the user by creating PreAuthenticatedToken.

Logout will work the following way:

Note that we assume here that the logout was initialized on SP. This is important because user will be redirected to this SP. Lets Imagine that user is on some page /secured_area and clicks “Logout”. At this moment local logout happens on SP. Then the user will be redirected to IdP on special URL /sso/logout that will manage the logout flow from all other services. As soon as the user has come from SP, IdP selects next SP for logout and redirects the user to it. The second SP upon completion redirects user back to IdP and in case there is no more SPs to logout from, does a local logout (point 5 on the scheme). After that, user is redirected back to SP where he began the logout.

There’s another scenario where user starts logout process on IdP, instead of SP. It looks like this:

Identity Provider

To implement IdP you should choose an application in your company that will be responsible for it. Like it is done in Google (Google Accounts) or Yandex (Yandex.Passport).

In this application we will install the first part of SSO: SingleSignOnIdentityProviderBundle

SingleSignOnIdentityProviderBundle is responsible for:

  • Generation of One-Time Passwords (OTP)
  • Saves the SP-name from which the user came
  • Functionality for logout from all SPs

Install via composer:

then, add this bundle to AppKernel:

Include routes /sso/login and /sso/logout from the bundle:

Next step is setting up IdP bundle:

Next step in setting up Security:

Now you need to register SP in your application. For this purpose create a class that will implement interface \Krtv\Bundle\SingleSignOnIdentityProviderBundle\Manager\ServiceProviderInterface

and register it in the service container using the tag:

Public API of this bundle

This bundle registers several services into service container. These services will help you customize SSO flow in the your application:

When configuration of IdP is completed the next step is configuration of SP-part.

Service Provider

Application that you want to authenticate user through IdP should have SingleSignOnServiceProviderBundle

SingleSignOnServiceProviderBundle is responsible for:

  • Automatic authentication via IdP

Install via composer:

then, add this bundle to AppKernel:

Include route /otp/validate/ for OTP validation:

Next step is setting up SP bundle:

To get “true” Single Sign On, you should choose http method as manager, and service  as a provider. For this you have to implement Krtv\SingleSignOn\Manager\Http\Provider\ProviderInterface interface.

Edit security.yml:

Public API of this bundle

This bundle registers several services into service container. This services will help you customize SSO flow in the you application:

The configuration of applications is completed.

To watch the updates for this bundle, star them on Github:

25 comments on “A quick way to build Single Sign On Authentication in Symfony2

  • This article is absolutely fantastic, as are the Symfony2 libraries. We’ll certainly be giving it a shot; thanks for all of your hard work!

  • Hello, I have implemented your SSO project 0.3 versions, with symfony 2.8.
    I have been debugging SP and idP, it almost work to the end, but there is problem with UriSigner, and on both sides Malformed uri exceptions are produced. Have you noticed this problem already, it seems that _hash is not properly detected while it duplicates on idP. If we can talk about this problem, I can provide a fix for it and later push request.

    • I found one thing, which is not stated in README.md:
      secret_parameter: secret # Have to be same across all apps
      I had different secret string across all apps, so pay attention:)
      (Have first checked it myself and then found out this blog, as a confirmation :)

  • One should not assume that acronyms are understood by everyone. There are those, myself included, who want to understand this but do not know all the terms. For example, if OTP means one-time password, then say that and include OTP in parentheses following the full text. Later use of the acronym then becomes meaningful. The article becomes substantially more accessible when terms are defined.

  • Hi,

    thanks a lot for this article and your bundle.

    Juste one thing. How can we handle the ROLES of the connected people. Are the credentials transmitted with the OTP ? It’s not very clear to me. What is the best practice ?

    Thanks in advance.

  • Hello!

    Can I also use this bundle to implement the SSO in an IDP built in Symfony and an SP that is non-Symfony?

    Thanks!

  • Experiencing this error when SP makes request to my IDP server. Anyone know where the problem could be?

    “Target path not specified” at ‘vendor/korotovsky/sso-idp-bundle/src/Krtv/Bundle/SingleSignOnIdentityProviderBundle/Controller/SingleSignOnController.php line 29

  • my server sends request in the following format

    ?SAMLRequest=lZHLasMwEEV%2FxTut…….

    Do I sent Idp sso url: to /internal/v1/sso which gives me a malformed url error or /sso/login which tells me “Target path not specified”

  • I did setup a symfony app as IdP and another as SP as described.
    SSO works well for logout (when I logout from SP I am logged out at IdP as well).
    But SP does not notice when I am logged in at IdP.
    The fm_otp table in my db is still empty and untouched.
    What did I do wrong?

  • Now it works well but I have to set anonymous:false for the firewall.
    Is there a way to allow anonymous login and create a link to SSO?

    • There is no ready-to-go solution for that. But if you understand how SSO works internally, you can build this link, yes. Here is an example, because I had exactly the same use-case.

      Basically you have two options for that:

      1) throw Symfony\Component\Security\Core\Exception\AccessDeniedException(). It will triggeran built-in entrypoint and redirects user to SSO login endpoint.

      2) Create an anonymous form on SP and submit it (action attribute) to your login-in form on IdP (We did it in this way). With the extra parameter “service”. On IdP we had Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface implementation and checked for “service” parameter and if it was presented we performed some logic. Keep in mind that every URL for /sso/login controller action have to be signed with “@sso_identity_provider.uri_signer” service. Also useful service is “@sso_identity_provider.service_manager”

  • Great Thanks for your help.
    Seems like I’ve found a way:
    1. I allowed anonymous login in my firewall
    2. I required ROLE_USER for /login in access_control
    Now I have a startpage that allows anonymous and login url is redirected to sso.

  • I realized that the generated OTP’s were never deleted and may flood your IdP’s database. That’s why I installed an EventListener at my IdP:


    // src/AppBundle\EventListener/RemoveOldOneTimePasswords
    getClassMetadata()->getName() == $class ) {
    self::$called = true;
    $entityManager = $event->getEntityManager();
    $entityManager->createQuery(
    "DELETE FROM $class o
    WHERE DATEADD(o.created, INTERVAL 24 HOUR) execute();
    $entityManager->createQuery(
    "UPDATE $class o
    SET o.used = 1
    WHERE o.used = 0 AND DATEADD(o.created, INTERVAL 1 HOUR) execute();
    }
    }


    // app/config/services.yml
    services:
    app.event_listener.remove_old_otp:
    class: AppBundle\EventListener\RemoveOldOneTimePasswords
    tags: [{name: doctrine.event_subscriber}]
    public: false

    • Please delete my last post cause something went wrong with it.
      I can send you the files if you want.

  • Hi Korotovsky
    Thank you for your contribution with your excellent code.
    I am impressed for your SSO solution and decided to adopt the code into Mautic plaftform.
    As you know, Mautic is a representative Symfony project.
    So we are going to implement Mautic SSO and found your solution with Symfony.
    But I have obstacle to migrate your symfony code to Mautic.
    I have made your code as a Mautic plugin, installed the plugin into Mautic.
    Mautic customized configuration style, I have difficulty to config security.php because it is so different with original Symfony form.
    When I add sso into main firewall and set provider as user-provider.
    Mautic made error “Symfony\Component\Debug\Exception\ContextErrorException: PHP Error – Argument 1 passed to MauticPlugin\MauticSingleSignOnBundle\Authentication\Provider\OneTimePasswordAuthenticationProvider::__construct() must implement interface Symfony\Component\Security\Core\User\UserProviderInterface, string given, called in C:\work\mautic\app\cache\dev\appDevDebugProjectContainer.php on line 14896 and defined”.
    I debugged many times to find solution but I was not familiar with Symfony, so I still couldn’t find the reason.
    I will be appreciate you to help me to find a right solution and give instructions on it.
    Thanks for your time.
    Best regards,
    Petar

Leave a Reply

Your email address will not be published. Required fields are marked *